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', ... });
Related
I would like to use axios to fetch the values from API in react.js, and set it as a form, but it doesn't display any fetched data at all.
export default function Review() {
const [fetchedData, setFetchedData] = useState([]);
const [fetchedlanguage, setlanguage] = useState([]);
useEffect(() => {
const getStudent = async () => {
const stu = await axios.get('http://localhost:8000/students/');
setFetchedData(stu.data.students[0]);
setlanguage(stu.data.students[0].languages)
};
getStudent()
},[]);
console.log("student: ", fetchedData);
const [formdata, setformdata] = useState({
availability: 6,
preference:'201, 301',
experience:'201',
language:fetchedlanguage[0],
background:fetchedData.background,
});
Even though the console.log shows the data correctly, when I set the form here, how come there is no updates on data?
Control it all in one place. You will want to spread the original values over the setformdata because it's immutable. I'm not sure what all the API returns so continue to override each formdata property that you get back from the API.
export default function Review() {
const [formdata, setformdata] = useState({
availability: 6,
preference:'201, 301',
experience:'201',
language: 'english',
background: 'initial-background',
});
useEffect(() => {
const getStudent = async () => {
const stu = await axios.get('http://localhost:8000/students/');
const student = stu.data.students.length > 0 ? stu.data.students[0] : {};
setFormData({
...formdata,
langauge: student.languages,
// TODO: continue to override the formData from student returned from API
});
};
getStudent()
}, []);
// TODO: use formdata to feed into form
return null;
}
I have a situation where I tried to fetch data when user login , and my structure is I have two redux slice one is userData other one is UserCartData and
when user login ,if login success ,
then I will dispatch data into UserData ,
3, then I write a useEffect to check if there's userData ,
I will then fetch UserCartData with UserData
But the thing is I can't get useGetxxxQuery work under useEffect,here's my code
const Login = () => {
const user = useAppSelector(state=> state.auth);
const dispatch = useAppDispatch();
const [login] = useLoginMutation();
const [showPassword,setShowPassword] = useState<boolean>(false);
useEffect(() => {
if (user!==null){ //fecth userCart data with userData
const {data ,isLoading,isFetching }= useGetCartByIDQuery(user.user?._id!)
}
}, [dispatch])
return (
<Container>
<Wrapper>
<Title>SIGN IN</Title>
<Formik
initialValues={{ email: "", password: "" }}
validationSchema={Yup.object({
password: Yup.string()
.min(8, 'Must be 8 characters or higher')
.required(),
email: Yup.string().email('Invalid email address').required(),
})}
onSubmit = { async (values, actions) => {
try{
const result = await login(values);
if("data" in result){
//console.log(result.data.data)
dispatch(setCredentials({user:result.data.data.findUser,token:result.data.data.cookie}))
}else{
const err = (result.error as RequestError).data.message
if(err.includes("password")){
actions.setErrors({password:err})
}else if(err.includes("Facebook")){
actions.setErrors({email:err})
}
}
}catch(err){
console.log(err)
}
}}>
//....unrelevant code
You don't need a useEffect here, you can simply use skipToken:
import {skipToken} from '#reduxjs/toolkit/query'
const {data, isLoading, isFetching } = useGetCartByIDQuery(user.user ? user.user._id : skipToken)
You can not use hooks inside other hooks. It is a rule!!! Source: https://reactjs.org/docs/hooks-rules.html Instead of this you can change useGetCartByIDQuery hook and use only a function from there: ex: const { startFetch } = useGetCartByIDQuery
useEffect(() => {
startFetch('your arguments')
}, [])
I am trying to do backend for my website and I have login, signup, reset password etc. all working. What I am trying to do now is when user log in or sign up, AuthContext to check for file that match his UID and if exist store it to variable or if not exist create it and store it to variable. Just cant get it work. Best i got so far is code at bottom but problem there is when I log out user I am getting all sort errors because user not exists anymore.
my context file look like this so far and everything is working:
import { createContext, useContext, useEffect, useState } from "react";
import { auth, db } from "../firebase";
export const AuthContext = createContext();
export const useAuth = () => {
return useContext(AuthContext);
};
const AuthContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [currentUserDoc, setCurrentUserDoc] = useState(null);
const [loading, setLoading] = useState(true);
const signup = (email, password) => {
return auth.createUserWithEmailAndPassword(email, password);
};
const login = (email, password) => {
return auth.signInWithEmailAndPassword(email, password);
};
const logout = () => {
return auth.signOut();
};
const resetPassword = (email) => {
return auth.sendPasswordResetEmail(email);
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = { currentUser, currentUserDoc, signup, login, logout, resetPassword };
return <AuthContext.Provider value={value}>{!loading && children}</AuthContext.Provider>;
};
export default AuthContextProvider;
I tryed to change useEffect hook to this but can't get it done right:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
setCurrentUser(user);
const userDoc = db.collection("users").doc(user.uid);
await userDoc.get().then((doc) => {
if (doc.exists) {
setCurrentUserDoc(doc.data());
} else {
doc.set({
email: user.email,
first_name: "",
last_name: "",
country: "",
organization: "",
});
}
});
setLoading(false);
});
return unsubscribe;
}, []);
This is error when there is no user logged in:
Code where I am trying to use it:
import styles from "./Header.module.scss";
import { useAuth } from "../../contexts/AuthContext";
const Header = () => {
const { logout, currentUserDoc } = useAuth();
return (
<header className={styles.header}>
<div></div>
<div className={styles.header__user}>
{currentUserDoc.email}
<button onClick={logout}>Log out</button>
</div>
</header>
);
};
export default Header;
The onAuthStateChanged observer will trigger when the user logs in or logs out. In case the user has logged out, the user object will be null and hence you will get an error "TypeError: Cannot read property 'uid' of null". You should check if the user is still logged in inside of the auth observer.
const unsubscribe = auth.onAuthStateChanged(async (user) => {
// Check if user is present (logged in) or absent (logged out)
if (!user) {
// user has logged out
console.log("No User")
} else {
// Add the required documents
setCurrentUser(user);
const userDoc = db.collection("users").doc(user.uid);
const doc = await userDoc.get()
if (doc.exists) {
setCurrentUserDoc(doc.data());
} else {
await userDoc.set({
email: user.email,
first_name: "",
last_name: "",
country: "",
organization: "",
});
}
}
})
I am trying to make an update user page with previous information to be rendered inside the input fields. Console.log returns the correct value but its not showing up as the initial value inside of the useState.
Getting previous user bio
function EditProfile(props) {
const user = useSelector(state => state.user);
const [profile, setProfile] = useState([])
const userId = props.match.params.userId
const userVariable = {
userId: userId
}
useEffect(() => {
axios.post('/api/users/getProfile', userVariable)
.then(response => {
if (response.data.success) {
console.log(response.data)
setProfile(response.data.user)
} else {
alert('Failed to get user info')
}
})
}, [])
console.log(profile.bio);
Heres what I am currently using to display the input field. (edited for brevity)
const [bio, setBio] = useState("");
const handleChangeBio = (event) => {
console.log(event.currentTarget.value);
setBio(event.currentTarget.value);
}
return (
<label>Bio</label>
<TextArea
id="bio"
onChange={handleChangeBio}
value={bio}
/>
)
Was trying to do this before but object was not showing up as the useState initial value
const [bio, setBio] = useState(User.bio);
Back-end - I know that $set overrides all information, so was trying to render the previous information inside of the input fields so it would not be overrided with blank values.
router.post('/edit', auth, (req, res)=> {
console.log(req.body.education)
User.updateMany(
{ _id: req.user._id },
[ {$set: { bio: req.body.bio}},
{$set: { industry: req.body.industry}},
{$set: { jobTitle: req.body.jobTitle}},
],
(err)=>{
if (err) return res.json({success: false, err});
return res.status(200).send({
success: true
});
});
});
Create some custom component and put User as props and you will see that you get data.
const [User, setUser] = useState([])
better to change to
const [user, setUser] = useState('')
You can get some issues because components starts with capital letter
And array as default value may error after first render
You can move it to separate component:
<Example user={user} />
const Example = (props) => {
const [bio, setBio] = useState(props.user.bio);
const handleChangeBio = (event) => {
console.log(event.currentTarget.value);
setBio(event.currentTarget.value);
}
return (
<label>Bio</label>
<TextArea
id="bio"
onChange={handleChangeBio}
value={bio}
/>
)
}
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) => {
});
}
};