I have a scenario where an async function is called on button click and the return value is setting the state value. After that, another function is called which needs the previously set value. As it is inside function I am not able to use useEffect. How to achieve this?
const [user, setUser] = React.useState(null);
const handleSignIn = async () => {
const result = await Google.logInAsync(config);
const { type, idToken } = result;
setUser(result?.user);
if (type === "success") {
AuthService.googleSignIn(idToken)
.then((result) => {
const displayName = `${user?.givenName} ${user?.familyName}`;
signIn({
uid: user.uid,
displayName: displayName,
photoURL: user.photoUrl,
});
})
.catch((error) => {
});
}
};
Here, handleSignIn is called on the button click and user state value is set from the result achieved from the Google.logInAsync. Then AuthService.googleSignIn is called and when success the user object is used there but it not available sometimes.
cbr's comment hits the nail on the head. You need to wrap everything following setUser in its own useEffect, which will depend on the user state variable. Like this:
const [result, setResult] = React.useState(null);
const handleSignIn = async () => {
const result = await Google.logInAsync(config);
setUser(result);
};
useEffect( () => {
if (result) {
const { type, idToken, user } = result;
if (type === "success") {
AuthService.googleSignIn(idToken)
.then((result) => {
const displayName = `${user?.givenName} ${user?.familyName}`;
signIn({
uid: user.uid,
displayName: displayName,
photoURL: user.photoUrl,
});
})
.catch((error) => {
});
}
}
}, [result])
What happens here is that your handleSignIn sets the user variable. Your useEffect runs whenever the user variable is updated. If it exists, it will run your AuthService code with the new user value.
Alternatively, you can skip using useEffect altogether by just referencing your result.user directly. Extract it from result along with the type and idToken, and use it directly. You can still save it to state with your setUser function if you need it later:
const [user, setUser] = React.useState(null);
const handleSignIn = async () => {
const result = await Google.logInAsync(config);
const { type, idToken, user } = result;
setUser(user);
if (type === "success") {
AuthService.googleSignIn(idToken)
.then((result) => {
const displayName = `${user?.givenName} ${user?.familyName}`;
signIn({
uid: user.uid,
displayName: displayName,
photoURL: user.photoUrl,
});
})
.catch((error) => {
});
}
};
Related
I am trying to log currentUser details to console to see if user is logged in or not. But it is logging again n again and make a loop of currentUser in console.
Here is the code
const [currentUser, setCurrentUser] = useState(null);
const unSubscribeFromAuth = useRef(null);
useEffect(() => {
unSubscribeFromAuth.current = auth.onAuthStateChanged(async userAuth => {
if (userAuth) {
const userRef = await createUserProfileDocument(userAuth);
userRef.onSnapshot(snapShot => {
setCurrentUser({
id: snapShot.id,
...snapShot.data()
})
console.log(currentUser);
})
}
else {
setCurrentUser(userAuth);
}
})
return () => { unSubscribeFromAuth.current() };
}, [currentUser]);
and here is the function i am importing
export const createUserProfileDocument = async (userAuth, additionalData) => {
if (!userAuth) return;
const userRef = firestore.doc(`users/${userAuth.uid}`);
const snapShot = userRef.get();
console.log(snapShot);
if (!snapShot.exists) {
const { displayName, email } = userAuth;
const createdAt = new Date();
try {
await userRef.set({
displayName,
email,
createdAt,
...additionalData
})
}
catch (error) {
console.log('error creating user', error.message);
}
}
return userRef;
}
I tried to remove dependency but it says
React Hook useEffect has a missing dependency: 'currentUser'. Either include it or remove the dependency array
Since you are updating currentUser in the useEffect and the useEffect is triggered every time currentUser changes, you are seeing the infinite loop. You need to remove currentUser from the dependency array.
useEffect has a missing dependency is coming from console.log. If you remove it, it will go away.
React code
import React, { useEffect, useState } from "react";
import { getDocs, collection } from "firebase/firestore";
import { auth, db } from "../firebase-config";
import { useNavigate } from "react-router-dom";
function Load() {
const navigate = useNavigate();
const [accountList, setAccountList] = useState([]);
const [hasEmail, setHasEmail] = useState(false);
const accountRef = collection(db, "accounts");
Am i using useEffect correctly?
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
checking whether email exists
const emailCheck = () => {
if (accountList.filter((e) => e.email === auth.currentUser.email)) {
setHasEmail(true);
} else {
setHasEmail(false);
}
};
Redirecting based on current user
const direct = () => {
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
return <div></div>;
}
The code compiles but doesn't redirect properly to any of the pages.
What changes should I make?
First question posted excuse me if format is wrong.
There are two problems here:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
In order:
Since getAccounts is asynchronous, you need to use await when calling it.
But even then, setting state is an asynchronous operation too, so the account list won't be updated immediately after getAccounts completes - even when you use await when calling it.
If you don't use the accountList for rendering UI, you should probably get rid of it as a useState hook altogether, and just use regular JavaScript variables to pass the value around.
But even if you use it in the UI, you'll need to use different logic to check its results. For example, you could run the extra checks inside the getAccounts function and have them use the same results as a regular variable:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
const result = data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
setAccountList(result);
emailCheck(result);
direct();
};
getAccounts();
}, []);
const emailCheck = (accounts) => {
setHasEmail(accounts.some((e) => e.email === auth.currentUser.email));
};
Alternatively, you can use a second effect that depends on the accountList state variable to perform the check and redirect:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
});
useEffect(() => {
emailCheck();
direct();
}, [accountList]);
Now the second effect will be triggered each time the accountList is updated in the state.
I'm not sure why but for some reason I don't have the value of old state when calling update, it always seems to have the value of default state.
const defaultState:RootState = {
loadSessions:(groupId:string,lectureId:string)=>{},
loadSession:(sessionId:string)=>()=>{},
userJoined:(user:IUser)=>{}
};
const TSessionContext = React.createContext<RootState>(defaultState);
export const TSessionProvider: FC = ({ children }) => {
const [state, setState] = useState<RootState>(defaultState);
const loadSession = (sessionId:string)=>{
console.info("Calling load session");
const unsubSession = db.collection(COLLECTION_REF).doc(sessionId)
.onSnapshot((snapshot) => {
const session = snapshot.data() as ISession;
console.info("setting session",state);
setState({
...state,
session
});
});
const unsubUsers = db.collection(`${COLLECTION_REF}/${sessionId}/users`)
.onSnapshot((querySnapshot) => {
const users:IUser[] = [];
querySnapshot.forEach((doc) => {
users.push(doc.data() as IUser)
});
console.info("setting users",state);
setState({
...state,
users
});
});
return ()=>{
unsubUsers();
unsubSession();
};
}
return (
<TSessionContext.Provider
value={{
...state,
loadSession,
}}
>
{children}
</TSessionContext.Provider>
);
};
export const useTSession = () => useContext(TSessionContext);
Here I always get output "setting session" with default state value and "setting users" with default state value.
Code where I am using this hook:
const {loadSession,session,users} = useTSession();
useEffect(()=>{
if(sessionId&&!session){
const unsub = loadSession(sessionId);
return () => {
unsub()
}
}
},[]);
Then in my main component I get session first with user undefined.
And then user with session undefined.
If I add another hook that fetches more data, then it has the same issue. So I'm really not sure what is the problem here.
If I split my hooks internal state like this:
const [iSession, setISession] = useState<any>();
const [iUser, setIUser] = useState<any>();
return (
<TSessionContext.Provider
value={{
...state,
users:iUser,
session:iSession,
loadSession,
}}
>
{children}
</TSessionContext.Provider>
);
Then it seems to work correctly, not sure why the other variant doesn't.
setState is asynchronous so when you call them like that you are overwriting the previous state. I haven't seen a pattern like the one you are using but you shouldn't try write state twice in the same function as you will face problems you could maybe update to this
const loadSession = async(sessionId:string)=>{
const newState = {}
console.info("Calling load session");
const unsubSession = await db.collection(COLLECTION_REF).doc(sessionId)
.onSnapshot((snapshot) => {
const session = snapshot.data() as ISession;
console.info("setting session",state);
newState.session = session
});
const unsubUsers = await db.collection(`${COLLECTION_REF}/${sessionId}/users`)
.onSnapshot((querySnapshot) => {
const users:IUser[] = [];
querySnapshot.forEach((doc) => {
users.push(doc.data() as IUser)
});
console.info("setting users",state);
newState.users = users
});
setState({
...state,
...newState,
});
return ()=>{
unsubUsers();
unsubSession();
};
}
I'm having a problem. been browsing some questions here but seems doesn't work for me.
I'm getting this error in my three pages when I'm using the useEffect.
This is the code of my useEffect
const UserDetailsPage = () => {
const classes = useStyles()
const [userData, setUserData] = useState({
_id: "",
name: "",
phone: "",
address: "",
birthdate: "",
gender: "",
messenger: "",
photo: "",
email: "",
})
const [open, setOpen] = useState(false)
const [loaded, setLoaded] = useState(false)
const { height, width } = useWindowDimensions()
const {
_id,
name,
phone,
address,
photo,
gender,
messenger,
birthdate,
email,
} = userData
useEffect(() => {
const user = getUser()
getUserById("/user/" + user.userId, user.token)
.then((data) => {
setUserData(data)
setLoaded(true)
})
.catch((error) => {
console.log(error)
})
}, [])
Short of getUserById returning a cancel token to cancel any inflight network requests, or an "unsubscribe" method, you can use a React ref to track if the component is still mounted or not, and not enqueue the state update if the component has already unmounted.
const isMountedRef = React.useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => isMountedRef.current = false;
}, []);
useEffect(() => {
const user = getUser();
getUserById("/user/" + user.userId, user.token)
.then((data) => {
if (isMountedRef.current) {
setUserData(data);
setLoaded(true);
}
})
.catch((error) => {
console.log(error);
});
}, []);
This is because of the async call in useEffect finishing and then attempting to setState after the page is no longer in focus.
It can be avoided by refactoring the useEffect like so:
useEffect(() => {
// created a boolean to check if the component is mounted, name is arbitrary
let mounted = true;
const user = getUser();
getUserById("/user/" + user.userId, user.token)
.then((data) => {
// only setState if mounted === true
if (mounted) {
setUserData(data);
setLoaded(true);
}
})
.catch((error) => {
console.log(error);
});
// set mounted to false on cleanup
return () => {
mounted = false;
};
}, []);
What's different here is that I use a mounted boolean to check if the page is currently mounted. By wrapping the setState call inside an if state, I can check if it's safe to setState, therefore avoiding the error.
Additional reading
This happens when your component is unmounting before setting your state. Try this code below to check if the component is mounted or not.
useEffect(() => {
let isMounted = true; // add a flag to check component is mounted
getUserById("/user/" + user.userId, user.token)
.then((data) => {
if(mounted) { // set state only when component is mounted
setUserData(data)
setLoaded(true)
}
})
.catch((error) => {
console.log(error)
})
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);
Don't use async tasks in useEffect. Define an async function and call in your useEffect.
Example:
const getSTH = async() =>{
getUserById("/user/" + user.userId, user.token)
.then((data) => {
if(mounted) { // set state only when component is mounted
setUserData(data)
setLoaded(true)
}
})
.catch((error) => {
console.log(error)
})
}
useEffect (()=>{
getSTH();
},[])
I think this approach will help you.
I'm trying to sign a user in, and update my global context with the user data. To keep the user signed in I'm storing their data in local storage.
I'm using react-hooks to take care of the state, hence I have defined a state: let [userData, setUserData] = useState({});.
Since I wan't to keep the user signed in I store their data in local storage during sign in. This works and the data does in fact get stored in local storage.
My problem is however that I can't set the initial userData state equal to the current data from local storage. In other words the userData state gets reset to default on reload.
I thought that getting the initial data from local storage and assigning it to state inside the useEffect hook would work. But the state does not update when calling setUserData inside useEffect.
AuthContext.js:
import React, { createContext, useState, useEffect } from 'react';
export const AuthContext = createContext();
const AuthContextProvider = props => {
let [userData, setUserData] = useState({});
const loginUser = (data) => {
localStorage.setItem('userData', JSON.stringify({
key: data.key,
id: data.id,
email: data.email,
first_name: data.first_name,
last_name: data.last_name
})); // Save the user object in local storage
setUserData({
key: data.key,
id: data.id,
email: data.email,
first_name: data.first_name,
last_name: data.last_name
}); // Set user data
};
const logoutUser = () => {
localStorage.removeItem('userData');
setUserData({}); // Empty user data state
newToast('Successfully signed out');
};
useEffect(() => {
const localUser = JSON.parse(localStorage.getItem('userData'));
if (localUser && localUser.key) {
setUserData({
key: localUser.key,
id: localUser.id,
email: localUser.email,
first_name: localUser.first_name,
last_name: localUser.last_name
}); // Set user data
}
}, [])
return (
<AuthContext.Provider value={{ userData, loginUser, logoutUser, newToast }}>
{props.children}
</AuthContext.Provider>
)
}
export default AuthContextProvider;
Signin.js:
const Signin = props => {
let [loading, setLoading] = useState(false);
let [formError, setFormError] = useState(false);
const { userData, loginUser, newToast } = useContext(AuthContext);
const { register, handleSubmit, errors, setError, clearError } = useForm();
const onSubmit = e => {
setLoading(true);
setFormError(false);
clearError(); // Clear all erros on form
axios
.post('users/auth/login/', {
headers: { 'Content-Type': 'application/json' },
email: `${e.email}`,
password: `${e.password}`,
})
.then(res => {
const { data } = res
loginUser(data);
newToast('Successfully signed in');
})
.catch((error) => {
const { data } = error.response;
console.log(data);
data.email && setError("email", "", data.email);
data.password1 && setError("password", "", data.password1);
setFormError(true);
})
setLoading(false);
};
return ( ... );
}
Updated answer (Aug. 15, 2022):
Since accessing the local storage on every render is expensive, it is preferred to only access it during the initial render (see Wayne Ellery's comment).
So quoting Erol's solution:
const [user, setUser] = useState([], () => {
const localData = localStorage.getItem('userData');
return localData ? JSON.parse(localData) : [];
});
Original answer:
So I figured out a solution!
In AuthContext.js i didn't need to assign the state in useEffect.
Instead I get the initial data directly when defining the state hooks:
const localUser = JSON.parse(localStorage.getItem('userData')) || {};
let [userData, setUserData] = useState(localUser);
That way I don't need the useEffect hook at all.
I hope this is the recommended way of doing it.
If I understand your question, you could do the following:
const [user, setUser] = useState([], () => {
const localData = localStorage.getItem('userData');
return localData ? JSON.parse(localData) : [];
});
How about to use useReducer like this?
const [user, setUser] = useReducer((prev, cur) => {
localStorage.setItem('userData', JSON.stringify(cur));
return cur;
}, JSON.parse(localStorage.getItem('userData')));
You can call
setUser({ key: '1', ... });