reactjsredux

Issue about protectedRoute, redirecting when authState hasn't been updated yet


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]);
};

Solution

  • Issue

    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.

    Solution Suggestion

    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}</>;
    };