I want to send user to backend in function handleJoin().
After setUser is called, the initial data not changed.
How to fix it without using class
App.js
import React, { useState } from "react";
import Join from "./components/Join";
const App = () => {
const [user, setUser] = useState({ });
// Send user data to backend
const handleJoin = (input) => {
console.log(input); // > {name: "myname"}
setUser(input); // Not working!
console.log(user); // > { }
// I want to connect backend here
// But the user objet is empty
};
return <Join onJoin={handleJoin} />;
};
export default App;
user will be updated on the next render after calling setUser.
import React, { useState, useEffect } from "react";
import Join from "./components/Join";
const App = () => {
const [user, setUser] = useState(null);
// This is a side effect that will occur when `user`
// changes (and on initial render). The effect depends
// `user` and will run every time `user` changes.
useEffect(() => {
if (user) {
// Connect to backend here
}
}, [user])
// Send user data to backend
const handleJoin = (input) => {
console.log(input);
setUser(input);
};
return <Join onJoin={handleJoin} />;
};
export default App;
State update is not synchronous so it will not update user object right away but it will be updated asynchronously. So Either you can use input which is going to be user value to be sent to backend or you can use useEffect() hook which will be triggered when user value will be udpated
useEffect(() => {
// this will be triggered whenever user will be updated
console.log('updated user', user);
if (user) {
// connect to backend now
}
}, [user]);
Related
I am trying to get data from AsyncStorage and eventually map this data to a list of buttons on my home screen. The data is saved to AsyncStorage from an API call that is made upon login.
In my async function, I am able to successfully retreive the data from AsyncStorage and parse it into JSON format, and then log it to the console. It looks like this:
{
1 : {title:"Timesheet",component_name:"Timesheet"}
2 : {title:"Personal Info",component_name:"PersonalInfo"}
3 : {title:"Employee Directory",component_name:"EmployeeListing"}
}
The problem I am running into is that I can't save this data to my useState variable and then render it into the component after useState is updated by my async function. Every time I try to access this data, I either get null or a Promise object. How can I access the data after useState is updated? Do I need to use a different React hook to call the Async function?
Here is the code that I am using:
import { Text, View, StyleSheet } from 'react-native';
import { useState, useEffect } from 'react';
import AsyncStorage from '#react-native-async-storage/async-storage';
export default function HomeScreen() {
const [buttonData, setButtonData] = useState(null);
useEffect (() => {
const loadHomeScreenButtons = async () => {
try {
const buttons = await AsyncStorage.getItem('app_screens').then(screens => {
// Parse the JSON data from its stored string format into an object.
let app_screens_json = JSON.parse(screens);
let app_screens_list = app_screens_json.app_screens;
console.log(app_screens_list); // This outputs the data to the console.
setButtonData(app_screens_list); // Trying to set the button data in useState.
return app_screens_list;
});
}
catch (e) {
console.log(e)
}
}
loadHomeScreenButtons();
}, [])
return (
<View style={home_styles.container}>
<Text>{buttonData[1]["title"]}</Text>
</View>
);
}
You just need to render a loading component until your data is fetched.
{ buttonData?.length?
<Text>{buttonData[1]["title"]}</Text> : <Text>...loading</Text>
}
You are getting an error as you are trying to access a property that does not exist at the render.
Try this way
const loadHomeScreenButtons = useCallback(async () => {
try {
const screens = await AsyncStorage.getItem('app_screens')
const app_screens_json = JSON.parse(screens)
setButtonData(app_screens_json.app_screens)
}
catch (e) {
console.log(e)
}
}, []);
useEffect(() => {
loadHomeScreenButtons();
}, [loadHomeScreenButtons]);
the code I have now created is working, but I think it is somehow suboptimal, as it does to many database reads
As far as I understand it, the "onAuthStateChange" can be understood like an useEffect hook, which gets called whenever the user authentication state changes (login, logout). Whenever this happens, the database should be checked for the username which has been chosen by the user. But if I take a look at the console the docSnap is logged a fair amount of times which to me indicates that the function gets called more often than just when the user logs in / logs out.
Context Component
import { createContext } from "react/cjs/react.production.min";
import { onAuthStateChanged } from "firebase/auth";
import { doc, getDoc } from "firebase/firestore";
import { auth,db } from "./firebase";
import { useState, useEffect } from "react";
export const authContext = createContext({
user: "null",
username: "null",
});
export default function AuthenticationContext(props) {
const [googleUser, setGoogleUser] = useState(null);
const [username, setUsername] = useState(null);
const [userID, setUserID] = useState(null);
onAuthStateChanged(auth, (user) => {
if (user) {
setGoogleUser(user.displayName);
setUserID(user.uid);
getUsername();
} else {
setGoogleUser(null);
}
});
const getUsername = async () => {
const docRef = doc(db, `users/${userID}`);
const docSnap = await getDoc(docRef);
if(docSnap.exists()){
setUsername(docSnap.data().username);
}
else{
setUsername(null);
}
};
return (
<authContext.Provider value={{ user: googleUser, username: username }}>
{props.children}
</authContext.Provider>
);
}
What is more, is that when I login with google and submit a username, the components do not get reevaluated - so a refresh is necessary in order for all the changes to take effect, this has something to do with me not updating state in the submitHandler of the login page. If you have some ideas on how I can do this more professionally please let me hear them. Thank you in advance!
Submit Handler on Login Page
const submitHandler = async (event) => {
event.preventDefault();
console.log(auth.lastNotifiedUid);
await setDoc(doc(db, "users", auth.lastNotifiedUid), {
displayName: user,
username: username,
});
await setDoc(doc(db, "usernames", username), { uid: auth.lastNotifiedUid });
};
As pointed out in the comments by Dharmaraj, you set multiple subscriptions to the authentication state. This is because you call onAuthStateChanged in the body of your component, so it is executed on every render.
To avoid this, you should wrap the function in useEffect so that you only subscribe on component mounting, and unsubscribe on unmounting:
export default function AuthenticationContext(props) {
/* ... */
React.useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => { /* ... */ });
return unsubscribe;
}, []);
/* ... */
}
TopBar.js is basically an AppBar component which handles authentication, if a user logs in, I get an object "user", how do I export this "user" object to App.js?
If I manage to export this to App.js, I would like to export it to another component create which can handle adding stuff to my db
I am using React and Firebase
This is my useEffect function
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((authUser) => {
if (authUser) {
// if user has logged in...
setUser(authUser)
if(authUser.displayName){ }
else{
return authUser.updateProfile({
displayName : userName
});
}
} else {
// if user has logged out...
setUser(null);
}
})
return () => {
// perform some cleanup actions
unsubscribe();
}
}, [user, userName]);
In the App.js file, you could create a context that handles this. In that case you have:
const AuthContext = createContext();
This can now be used to wrap the whole app using the Provider component like so:
// the user value comes from the state created
<AuthContext.Provider value={{user}}>
</AuthContext.Provider>
Then you can access this user value from any component using useContext hook:
const { user } = useContext(AuthContext);
// you can go on and access the `user` value anywhere in the component
you can visit this article explaining it in-depth
I have a component that uses react hooks and socketio:
import React, { useState, useEffect } from "react";
import io from "socket.io-client";
const Live = () => {
const [messages, setMessages] = useState([]);
const ENDPOINT = "localhost:5000";
useEffect(() => {
socket = io(ENDPOINT);
socket.on("message", (message) => {
setMessages((messages) => [message, ...messages]);
//setMessages([message, ...messages]); //It doesn't work
});
}, [ENDPOINT]);
// rest of the code
};
If I use the setMessages([message, ...messages]), it doesn't work and upon receiving a new message from socket, all the previous message in messages array are gone. What is the reason behind this behavior?
I don't recommend useState here to do what you're doing. Instead, consider useReducer
const Live = () => {
const ENDPOINT = "localhost:5000";
const [{ messages }, dispatch] = useReducer((state, action) => {
if (action.type === 'NEW_MESSAGE') {
state.messages.push(action.message)
return state
}
return state
}, { messages: [] })
useEffect(() => {
socket = io(ENDPOINT);
socket.on("message", (message) => {
dispatch({ type: 'NEW_MESSAGE', message })
});
}, [ENDPOINT])
// rest of the code
};
from the React docs on useReducer
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
In your case, the next messages state depends on the previous messages state.
I have a provider component that sets the initial auth context from firebase-auth.
Everything works fine until I try to add persistence in the form of setting up an observer with onAuthStateChanged. This checks for auth and I update my state via dispatch method.
But this is causing an infinite loop. I added an unsubscribe function call, but this makes no difference,
Can anyone advise? thanks
AuthContext.js
import React from "react";
import * as firebase from "firebase/app";
//firebaseauth reducer
import { firebaseAuth } from "../reducers/AuthReducer";
export const Auth = React.createContext();
const initialState = { user: {} };
export const AuthProvider = (props) => {
const [state, dispatch] = React.useReducer(firebaseAuth, initialState);
const value = { state, dispatch };
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
dispatch({
type: "HYDRATE_AUTH",
payload: user,
});
});
unsubscribe();
return <Auth.Provider value={value}>{props.children}</Auth.Provider>;
};
What is happening is:
The component renders
It sets the auth listener
The listener fires and sets the state of the component
State update causes the component to rerender
The component adds another listener and everything repeats
Unsubscribing doesn't help because the component keeps rerendering and adding a new listener every time.
We can tell the component to set the listener only once by using useEffect():
AuthContext.js
import React from "react";
import * as firebase from "firebase/app";
//firebaseauth reducer
import { firebaseAuth } from "../reducers/AuthReducer";
export const Auth = React.createContext();
const initialState = { user: {} };
export const AuthProvider = (props) => {
const [state, dispatch] = React.useReducer(firebaseAuth, initialState);
const value = { state, dispatch };
React.useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
dispatch({
type: "HYDRATE_AUTH",
payload: user,
});
});
}, []);
return <Auth.Provider value={value}>{props.children}</Auth.Provider>;
};
By providing an empty dependency array to useEffect(), we tell it to run the callback only once, when the component initially renders, so the auth listener is set only once.
Thanks to #tomek-ch -- an alternate solution would be to prevent the re-render by storing the state (eg: keep the user bits in the component state) and then do not update the state if its already there (or the same).
In my case I just keep the user the first time, set a boolean state flag, and ignore subsequent events.