After login via /login page, it directs to /profile page without any issue. A refresh on profile page will direct to /login page and I've attached the console log image here! . The authState is stored in localstorage with redux-persist, checkAuth() is jwt accessToken stored in Cookie.
Solved: redux state is reset to initial state when page reloads, then redux-persists kicks in and rehydrates the state. Therefore, loading needs to stay true during rehydration. The answer is actually quite smart, "null for the third case where the app hasn't confirmed either way if the user is authenticated or not" A simpler yet efficient solution without adding the rehydration check in protectedroutes.
// src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
import useAuthGuard from '@/hooks/useAuthGuard';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export const ProtectedRoute = ({ children }: ProtectedRouteProps): JSX.Element => {
const { user, loading, isAuthenticated } = useSelector((state: RootState) => state.auth);
const location = useLocation();
console.log('Full Auth State:', { user, loading, isAuthenticated, currentPath: location.pathname });
// Use the custom hook to ensure authentication checks
useAuthGuard();
// console.log('Auth State:', { user, loading, currentPath: location.pathname });
console.log(`console->log ${location.pathname}`, user);
// If still loading (e.g., while checking tokens), render a loading spinner or placeholder
if (loading) {
return <div>Loading...</div>; // Replace with your app's loading spinner
}
// Check if user is not authenticated and not already on the profile page
if (!isAuthenticated || !user) {
// Redirect to login while preserving the intended destination
console.log('Redirecting to login...');
return <Navigate to="/login" state={{ from: location.pathname }} replace />;
}
return <>{children}</>;
};
export const useAuthGuard = () => {
const {user, isAuthenticated, loading } = useSelector((state: RootState) => state.auth);
const dispatch = useDispatch<AppDispatch>();
useEffect(() => {
const verifyAuth = async () => {
try {
// Check both authentication and user data
if (!loading && (!isAuthenticated || !user)) {
await dispatch(checkAuth()).unwrap();
}
} catch (error) {
console.error('Auth check failed, redirecting to login'); }
};
verifyAuth();
}, [isAuthenticated, loading, dispatch]);
};
user
is initially null
, loading
is initially false
, and isAuthenticated
is initially false
upon page reload, this goes straight through and renders the Navigate
component in ProtectedRoute
.
Since it seems you use the falsey loading
state to help trigger the authentication verification, I suggest updating the AuthState.isAuthenticated
type to also allow for null
for the third case where the app hasn't confirmed either way if the user is authenticated or not. This can be used to short-circuit and also render a loading indicator while the app loads and runs the auth verification.
interface AuthState {
user: User | null;
loading: boolean;
error: string | null;
isAuthenticated: boolean | null; // <-- true | false | null
}
const initialState: AuthState = {
user: null,
loading: false,
error: null,
isAuthenticated: null, // <-- initially null
};
export const ProtectedRoute = (
{ children }: ProtectedRouteProps
): JSX.Element => {
const {
user,
loading,
isAuthenticated
} = useSelector((state: RootState) => state.auth);
const location = useLocation();
useAuthGuard();
// If still loading (e.g., while checking tokens) or the `isAuthenticated`
// state is not verified yet, render a loading spinner or placeholder
if (loading || isAuthenticated === null) {
return <div>Loading...</div>;
}
// Check if user is not authenticated and not already on the profile page
if (!isAuthenticated || !user) {
// Redirect to login while preserving the intended destination
return <Navigate to="/login" state={{ from: location.pathname }} replace />;
}
return <>{children}</>;
};