How to implement protected routes with authentication (Context API, Redux)
Hold auth state in Context (or Redux), wrap private routes in a guard component that checks auth status, redirect unauthenticated users to login (preserving the intended destination), handle the loading state during the auth check, and remember client-side guards are UX — the server enforces real access.
Protected routes gate parts of the app behind authentication. The pieces: auth state, a guard, redirect handling, the loading state, and the understanding that this is UX, not security.
1. Auth state — Context or Redux
Hold the user/session globally:
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // checking session on load
useEffect(() => {
checkSession().then(setUser).finally(() => setLoading(false));
}, []);
return <AuthContext.Provider value={{ user, loading, login, logout }}>{children}</AuthContext.Provider>;
}
const useAuth = () => useContext(AuthContext);Redux is the same idea — an auth slice instead of context. Context is plenty for auth (low-frequency updates).
2. The guard component
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) return <FullPageSpinner />; // don't decide yet
if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
return children;
}
// usage (React Router v6):
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />Or a layout route wrapping all private routes with an <Outlet/>.
3. The loading state — the most-missed detail
On a fresh page load you don't yet know if there's a valid session (you're calling checkSession). If you skip the loading check, you flash the login page and redirect a logged-in user. Always: loading → spinner, then decide.
4. Redirect + return to destination
- Redirect unauthenticated users to
/login, passing the attempted URL (state.fromor a?redirect=param). - After successful login, send them back there instead of a generic home page.
- Use
replaceso the protected URL doesn't sit in history.
5. Role / permission gating
Extend the guard: if (!user.roles.includes(requiredRole)) return <Navigate to="/403" />. Or a <RequireRole role="admin"> wrapper.
6. The critical caveat — this is UX, not security
Client-side route guards only hide UI. Anyone can edit JS or call the API directly. Every protected resource must be enforced server-side — the API checks the token/session on every request. The frontend guard just gives a clean experience; it's not the access control.
Token handling note
Prefer the token in an HttpOnly cookie (XSS-safe) over localStorage; or token-in-memory with silent refresh. Handle expiry — a 401 from the API should clear auth state and bounce to login.
The framing
"Auth state in Context (or a Redux slice) including a loading flag for the initial session check. A ProtectedRoute guard renders a spinner while loading, redirects to login with the intended destination if unauthenticated, otherwise renders the route. Role-based variants extend the same guard. The key point: this is UX only — the server must enforce access on every request; client guards are bypassable."
Follow-up questions
- •Why is the loading state during the auth check so important?
- •How do you send the user back to their intended page after login?
- •Why are client-side route guards not real security?
- •Where should the auth token live, and why?
Common mistakes
- •Skipping the loading state and flashing the login page for logged-in users.
- •Treating client-side guards as actual access control.
- •Not preserving the intended destination through the login redirect.
- •Storing tokens in localStorage where XSS can steal them.
- •Not handling token expiry / 401s by clearing auth and redirecting.
Performance considerations
- •Auth state in Context is fine — it changes rarely. The initial session check is one request; gate rendering on it. Lazy-load protected route bundles so unauthenticated users never download them.
Edge cases
- •Token expiring mid-session.
- •Direct navigation to a deep protected URL on a cold load.
- •Role changes while the user is active.
- •Logout in one tab — syncing other tabs.
Real-world examples
- •React Router v6 ProtectedRoute / layout-route guard with <Outlet/>.
- •Redirect with state.from + post-login return-to-destination.