React.js, Auth Component does not redirect properly - javascript

I have created this Auth Component and it works fine. Except that, It does not redirect if the unauthenticated user tries to visit /dashboard.
The backend upon receiving /api/me request, knows the user by having the cookie. So I have (Cookie-Session) Authentication technique.
export const UserContext = createContext();
const Auth = ({ children }) => {
const [user, setUser] = useState(null);
const [gotUser, setGotUser] = useState(false);
const navigate = useNavigate();
const getUser = async () => {
const res = await fetch('/api/me');
const data = await res.json();
setUser(data);
if (user) {
setGotUser(true);
}
};
useEffect(() => {
if (!gotUser) {
getUser();
}
}, [user, gotUser, navigate]);
if (!user) {
navigate('/login');
}
console.log(user);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
So the main issue is that no redirection done. Also, The user passed to the context is not updated properly. Maybe because I am confused about what to use in useEffect .
Any help is appreciated.

Issues
There are a couple issues:
The "unauthenticated" state matches the "I don't know yet" state (i.e. the initial state value) and the component is redirecting too early. It should wait until the user state is confirmed.
The navigate function is called as an unintentional side-effect directly in the body of the component. Either move the navigate call into a useEffect hook or render the Navigate component to issue the imperative navigation action.
Solution
Use an undefined initial user state and explicitly check that prior to issuing navigation action or rendering the UserContext.Provider component.
const Auth = ({ children }) => {
const [user, setUser] = useState(); // <-- initially undefined
const navigate = useNavigate();
const getUser = async () => {
try {
const res = await fetch('/api/me');
const data = await res.json();
setUser(data); // <-- ensure defined, i.e. user object or null value
} catch (error) {
// handler error, set error state, etc...
setUser(null); // <-- set to null for no user
}
};
useEffect(() => {
if (user === undefined) {
getUser();
}
}, [user]);
if (user === undefined) {
return null; // <-- or loading indicator, spinner, etc
}
// No either redirect to log user in or render context provider and app
return user
? <Navigate to="/login" replace />
: <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};

useEffect runs after your JSX is rendered, so as your code is made, on a page refresh this if (!user) that calls navigate('/login') will always pass, as before the useEffect does its work, user is null, that inital value you gave to useState. Yet it's not redirecting because navigate does not work inside JSX, it should be replaced with Navigate the component.
Also, in getUser, you have this if (user) juste after setUser(data), that wouldn't work well as user won't get updated immediately, as updating a state is an asynchronous task which takes effect after a re-redner .
To fix your problems you can add a checking state, return some loader while the user is being verified. Also you can optimise a little bit your code overall, like getting ride of that gotUser state:
export const UserContext = createContext();
const Auth = ({ children }) => {
const [user, setUser] = useState(null);
const [checking, setChecking] = useState(true);
const getUser = async () => {
try {
const res = await fetch("/api/me");
const data = await res.json();
setUser(data);
} catch (error) {
setUser(null);
} finally {
setChecking(false);
}
};
useEffect(() => {
if (!user) {
getUser();
}
}, [user]);
if (checking) {
return <p>Checking...</p>;
}
if (!user) {
return <Navigate to="/login" replace />
}
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
export default Auth;

Related

React Native trigger useEffect from changes in AsyncStorage

So I'm working on a react native authentication screen. I'm storing the token in AsyncStorage (yea I know it's not the best solution).
So what's happening is when I log in, the token is stored, but the getItem on my Authentication.js screen is not being triggered, and the profile screen is not being called.
If I log in and then manually refresh the app, I am redirected to the profile screen.
Login.js
function Login({navigation}) {
const [signIn, {data}] = useMutation(USER_SIGNIN_MUTATION);
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
function handleLogIn() {
signIn({
variables: {
email: userName,
password: password,
},
});
}
useEffect(() => {
if (data != null) {
setToken();
}
});
const setToken = async () => {
try {
console.log('before');
await AsyncStorage.setItem('token', data.signIn.token);
console.log('after');
} catch (error) {
console.log(error);
}
};
return(
...
)
}
Authentication.js
function Authentication() {
const [localToken, setLocalToken] = useState(false);
useEffect(() => {
const fetchUser = async () => {
try {
console.log('before get');
const userData = await AsyncStorage.getItem('token');
if (userData !== null) {
setLocalToken(true);
}
} catch (error) {
console.log(error);
}
};
fetchUser();
}, [localToken]);
console.log(`auth screen - ${localToken}`);
return (
<NavigationContainer>
{localToken === true ? <ProfileStack /> : <AuthStack />}
</NavigationContainer>
);
}
export default Authentication;
also same happens with the logout function when fired. (the function runs, but I need to refresh the app to get back to the login screen)
Profile.js
function Profile({navigation}) {
function signOut() {
logOut();
}
const logOut = async () => {
try {
console.log('before clear');
await AsyncStorage.removeItem('token');
console.log('after clear');
} catch (error) {
console.log(error);
}
};
return (
...
)
}
I'm grateful for any insight on this.
useEffect(() => {
const fetchUser = async () => {
try {
console.log('before get');
const userData = await AsyncStorage.getItem('token');
if (userData !== null) {
setLocalToken(true);
}
} catch (error) {
console.log(error);
}
};
fetchUser();
}, [localToken]);
Here you added the localToken variable in the dependency array of the useEffect. So you are basically saying: run this effect only if the localToken variable changes. But you change that from within the effect only. So try to remove it and keep the dependency as []. This way the effect will run when the component is rendered.
About the fact that you have to refresh the page, it is important to understand why this happens.
<NavigationContainer>
{localToken === true ? <ProfileStack /> : <AuthStack />}
</NavigationContainer>
Here you are rendering ProfileStack or AuthStack based on the localToken value. When you logout, you remove the token from the AsyncStorage but this is not enough. You actually need to trigger a rerender in the Authentication component so the localToken is reevaluated. Basically, when you logout you also need to set setLocalToken(false). So you need to access setLocalToken function from the Profile component. You can pass this function as a prop or better you can use Context API

Cannot setState after receiving user from service provider

I am receiving the user properly from my provider but for some reason the state does not change with the user object.
This returns the user object properly:
console.log(receivedUser);
However, after I try to do this, there's no object:
setUser(receivedUser);
console.log(user);
What seems to be the issue here guys? Why doesn't the state change at all?
Full code:
const [user, setUser] = useState({})
// this prevents this providerValue changing unless value or setValue changes
const providerValue = useMemo(() => ({user, setUser}), [user, setUser])
useEffect(() => {
async function fetchUser(){
const receivedUser = await AuthService.getCurrentUser();
if (receivedUser) {
// console.log(receivedUser);
setUser(receivedUser);
console.log(user);
} else {
console.log("user not logged in");
}
}
fetchUser();
}, []);
updation of state is asynchronous. you need to use useEffect to console out the value of state. A seperate useEffect can be created which would be triggered every time the user in the state changes
const [user, setUser] = useState({})
// this prevents this providerValue changing unless value or setValue changes
const providerValue = useMemo(() => ({user, setUser}), [user, setUser])
useEffect(() => {
async function fetchUser(){
const receivedUser = await AuthService.getCurrentUser();
if (receivedUser) {
// console.log(receivedUser);
setUser(receivedUser);
console.log(user);
} else {
console.log("user not logged in");
}
}
fetchUser();
}, []);
useEffect(()=> {
console.log('user',user)
}, [user])
You can do it like that, first setUser is an async.
You have to understand the life circle of useState
Will give you an example how to handle this right and wrong..
see below to example..
const [text, setText] = useState("default");
useEffect(() => {
setText("test");
console.log(text) // this will print defult
}, [])
useEffect(() => {
console.log(text) // this will print test
}, [text])
setUser it's kind of asynchronous function. State of user doesn't change immediately after you call this function. If you want to handle when user state change add another on useEffect which checking if user state change:
useEffect( () => {
/* this will be call any time user state change */
if ( user ) {
/* do something if user was fetched */
}
}, [user])

apollo's useQuery data does not update after client.resetStore()

I am having an issue with useQuery from #apollo/client
I have a Navbar component
const Navbar = () => {
const { loading, data, error } = useQuery(GET_CURRENT_USER);
console.log('navbar', data)
return <ReactStuff />
}
And I also have a UserProfile component which allows the user to logout via a button. When the user presses the button, this code is ran:
const {
getCurrentUser,
changePassword,
changePasswordData,
logoutUser,
} = useUsers();
const logout = () => {
logoutUser();
localStorage.removeItem("authToken");
props.history.push("/");
};
useUsers is a custom hook which houses the resetStore function:
const useUser = () => {
const { client, loading, error, data, refetch } = useQuery(GET_CURRENT_USER);
const logoutUser = () => {
console.log("firing logout user");
client
.resetStore()
.then((data) => {
console.log("here in reset store success");
})
.catch((err) => {
console.log("here in error");
}); // causes uncaught error when logging out.
};
return {
// other useUser functions
logoutUser
}
}
Now I can't for the life of me figure out why the Navbar component, does not get updated when logout is pressed.
I have a withAuth higher-order component which does the exact same query, and this works absolutely fine. The user is sent to the /login page, and If I was to console.log(data) it is updated as undefined - as expected.
import { GET_CURRENT_USER } from "../graphql/queries/user";
/**
* Using this HOC
* we can check to see if the user is authed
*/
const withAuth = (Component) => (props) => {
const history = useHistory();
const { loading, data, error } = useQuery(GET_CURRENT_USER);
if (error) console.warn(error);
if (loading) return null;
if (data && data.user) {
return <Component {...props} />;
}
if (!data) {
history.push("/login");
return "null";
}
return null;
};
For some reason, this useQuery inside Navbar is holding onto this stale data for some reason and I can't figure out how to make it work correctly.
Update 1:
I've changed the logoutUser function to use clearStore() and the same thing happens, the Navbar is not updated, but I am redirected to /login and withAuth is working as intended.
const logoutUser = () => {
client
.clearStore()
.then((data) => console.log(data)) // logs []
.catch((err) => console.log(err)); // no error because no refetch
};
You're not waiting for the store to reset, probably the redirection and storage clean up happen before the reset completes, try doing that once it has finished
const logout = () => {
logoutUser(() => {
localStorage.removeItem('authToken');
props.history.push('/');
});
};
const logoutUser = onLogout => {
console.log('firing logout user');
client
.resetStore()
.then(data => {
onLogout();
})
.catch(err => {
console.log('here in error');
}); // causes uncaught error when logging out.
};
Check: do you have only ONE ApolloClient instance?
In some cases, if you configuring ApolloClient in the custom class or file you can implicitly create multiple apolloClient instances, for example by 'import' statement. In that case you clearing only one of the caches you made.

Custom hook getting undefined value then the correct object in react native?

Hello so am trying to make a custom hook where i get my user object from database and using it however my console log shows the the object is undefined shortly after object appears and this effect other function relying on it they only capture the first state and gives an error how can i fix that here is my code for the hook:-
import { useState, useEffect } from 'react'
import { storage, auth, database } from '../api/auth';
export default function useUser() {
const [user, setUser] = useState()
const userId = auth.currentUser.uid;
useEffect(() => {
getCurrentUser();
}, [])
const getCurrentUser = () => {
database.ref("users/" + userId).orderByChild('name').once("value").then(snapshot => {
setUser(snapshot.val())
})
}
return user
}
First of all you can assign default value to userObject.
const [user, setUser] = useState(null);
After place where you use useUser hook you can make a check is it null or not.
const user = useUser();
if (!user) {
return <Text>Loading...</Text>;
}
return(
<Text>{user.name}</Text>
);

can't perform a react state update on an unmounted component issue with useEffect

I'm trying to redirect my user to a private route. I'm using redux thunk to fetch user info from the database, with storeUser(), if the info exists then the user proceeds otherwise they get redirected back to the home page. However its not working as expected. Its redirecting back to the home page when It should be proceeding. I can do this using class based syntax and componentDidMount. I tried to counter this issue of no access to componentDidMount by using the authChecked state to determine when the component has finished rendering
const PrivateRoute = (props) => {
const [authChecked, handleAuthChecked] = useState(false);
const [isAuth, handleIsAuth] = useState(false);
useEffect(() => {
props
.storeUser()
.then(() => {
props.user.email ? handleIsAuth(true) : handleIsAuth(false);
handleAuthChecked(true);
})
.catch(() => {
handleAuthChecked(true);
});
}, [props]);
if (authChecked) {
return isAuth ? <props.component /> : <Redirect to="/" />;
}
return null;
};
const mapStateToProps = (state) => {
return {
user: state.user,
};
};
export default connect(mapStateToProps, { storeUser })(PrivateRoute);
The code will always redirect the user though. isAuth will never return true even though props.user.email is true. It runs and redirects before it has chance to run handleIsAuth(true)
You have 2 issues that may be causing the defects you see:
First issue is caused by function scope within useEffect and your callback for storeUser. Instead of relying on the callback to determine whether the user has an email address, just do that in your render condition and let redux + react render cycle help you out.
In addition, you should only call the storeUser action on mount. Not every time props updates.
For example:
const PrivateRoute = (props) => {
const [authChecked, handleAuthChecked] = useState(false);
useEffect(() => {
props
.storeUser()
.then(() => {
handleAuthChecked(true);
})
.catch(() => {
handleAuthChecked(true);
});
}, []);
if (authChecked) {
return !!props.user.email
? <props.component />
: <Redirect to="/" />;
}
return null;
};
const mapStateToProps = (state) => {
return {
user: state.user,
};
};

Categories

Resources