Back to React
React
medium
mid

How would you implement protected routes with authentication using Context API or 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.

6 min read·~18 min to think through

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:

jsx
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

jsx
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.from or a ?redirect= param).
  • After successful login, send them back there instead of a generic home page.
  • Use replace so 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.

Senior engineer discussion

Seniors implement the guard cleanly but lead with two senior points: the initial-loading state (skip it and you flash login at authenticated users) and that client guards are UX-only — the server enforces real access. They also raise token storage (HttpOnly cookies vs localStorage) and 401/expiry handling.

Related questions