I am using two route guards for two purposes, but in a special scenario they are not working properly and I think they are going in a race condition.
routing
<Router>
<Routes>
<Route element={<GlobalGuard />}>
<Route path="/login" element={<Login />} />
<Route element={<LocalGuard />}>
<Route path="/projects/*" element={<Projects />}>
...
</Route>
</Route>
</Routes>
</Router>
Global Guard
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [validUser, setValidUser] = useState(false);
const location = useLocation();
useEffect(() => {
(async () => {
try {
if (localStorage.getItem('token')) {
const localUser = JSON.parse(localStorage.getItem('user'));
if (localUser && localUser.hasOwnProperty('email')) {
const pingStatus = await pingUser({ email: localUser.email });
if (pingStatus == 200) {
dispatch(fillUser(JSON.parse(localStorage.getItem('user');
setValidUser(() => true);
setLoading(() => false);
} else {
localStorage.removeItem('user');
localStorage.removeItem('token');
setValidUser(() => false);
setLoading(() => false);
}
} else {
localStorage.removeItem('user');
localStorage.removeItem('token');
setValidUser(() => false);
setLoading(() => false);
}
} else {
localStorage.removeItem('user');
setValidUser(() => false);
setLoading(() => false);
}
} catch (err) {
setValidUser(() => false);
setLoading(() => false);
}
})();
}, [location.pathname]);
console.log(location.pathname); // for checking if render correct
return loading ? <ServerLoad /> : <Outlet context={validUser} />;
Local Guard
const hasValidJwt = useOutletContext();
console.log(hasValidJwt);
return hasValidJwt ? <Outlet /> : <Navigate to="/login" />;
login.jsx
const navigate = useNavigate();
const onSubmit = async (data) => {
try {
const res = await loginUser(data);
dispatch(setUser(res));
localStorage.setItem('token', res.token);
localStorage.setItem('user', JSON.stringify(res.user));
navigate('/projects');
}
...
};
when onSubmit code runs in login page, in ideal scenario, the useEffect of global guard should make the hasValidUser as true and page navigates to projects, but instead it only refreshes.
After days of debugging, I found out the the global guard's useEffect's console.logs run after the localguard's consoles and it logs false
, so it again navigates to login
. Even after the hasValidUser to true
, the page reamins at login.
Can someone please give their perspective.
In Short
when I click login first time, it only fills the local storage with appropriate values, on second time it actually goes to /projects
The issue is that the initial validUser
value is the same as the unauthenticated value, so when the LocalGuard
component mounts and checks ths validUser
value and it is falsey, the redirect to "/login"
is rendered.
There are actually three "auth" states: "authenticated", "unauthenticated", and "unknown". The code should start from the "unknown" state and wait for authentication to be confirmed prior to rendering the protected content or the redirect.
const GlobalGuard = () => {
const dispatch = useDispatch();
const { pathname } = useLocation();
const [loading, setLoading] = useState(true);
const [validUser, setValidUser] = useState(); // <-- initially undefined
useEffect(() => {
(async () => {
setLoading(true);
try {
const localUser = JSON.parse(localStorage.getItem('user'));
if (!localUser?.email)) {
throw new Error("no user email");
}
const pingStatus = await pingUser({ email: localUser.email });
if (Number(pingStatus) !== 200) {
throw new Error("non-200 pingUser status");
}
dispatch(fillUser(JSON.parse(localStorage.getItem('user');
setValidUser(true);
} catch (error) {
localStorage.removeItem('user');
localStorage.removeItem('token');
setValidUser(false);
} finally {
setLoading(false);
}
})();
}, [pathname]);
return loading ? <ServerLoad /> : <Outlet context={validUser} />;
};
const LocalGuard = () => {
const hasValidJwt = useOutletContext();
if (hasValidJwt === undefined) {
render null; // or loading spinner/indicator/etc
}
return hasValidJwt ? <Outlet /> : <Navigate to="/login" replace />;
};