Back to React
React
medium
mid

How would you protect routes that require authentication and redirect unauthenticated users to login?

A ProtectedRoute wrapper checks auth state; if unauthenticated, redirect to /login (preserving the intended URL for post-login return). Handle the loading state while auth resolves to avoid a flash. But remember: route guards are UX — the server must still authorize every request.

4 min read·~8 min to think through

Protecting routes is a wrapper component that gates rendering on auth state — with a few details that separate a working answer from a robust one.

The ProtectedRoute pattern

jsx
function ProtectedRoute({ children }) {
  const { user, status } = useAuth();
  const location = useLocation();

  if (status === "loading") return <FullPageSpinner />;   // auth not resolved yet

  if (!user) {
    // redirect to login, REMEMBERING where they wanted to go
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

// usage
<Route path="/dashboard" element={
  <ProtectedRoute><Dashboard /></ProtectedRoute>
} />

After login, send them back:

jsx
const from = location.state?.from?.pathname || "/";
navigate(from, { replace: true });

The details that matter

1. The loading state. Auth status is usually resolved asynchronously (a /me call or reading a cookie session). If you only check if (!user), you'll flash the login page before auth resolves, then bounce the user to the dashboard. You need a three-state model — loading | authenticated | unauthenticated — and render a spinner during loading.

2. Preserve the intended destination. Pass location in the redirect's state so after login the user lands where they were going — not always the homepage. Deep links should survive a login detour.

3. replace, not push. Use replace on the redirect so the protected URL isn't left in history (back button shouldn't bounce them).

4. Role/permission gating. Extend it: <ProtectedRoute requiredRole="admin"> redirects authenticated-but-unauthorized users to a 403 page, not login.

5. Layout-level guards. Instead of wrapping every route, wrap a shared layout route so all child routes are protected at once.

The non-negotiable

Route guards are UX, not security. They make protected pages unreachable in the UI and route users sensibly — but anyone can edit client state or call the API directly. Every protected API endpoint must authorize server-side. The guard's job is a smooth experience; the server is the actual gate.

The framing

"A ProtectedRoute wrapper that reads auth state. The details that make it robust: a three-state model — loading/authenticated/unauthenticated — so I render a spinner while auth resolves instead of flashing the login page; preserving the attempted URL in the redirect's state so post-login I send them back there; using replace so history stays clean; and a role variant for authorization. And the load-bearing caveat — this is UX, the server still authorizes every request."

Follow-up questions

  • Why do you need a loading state in the route guard?
  • How do you send the user back to the page they originally wanted?
  • Why use replace instead of push for the redirect?
  • How is route protection different from real authorization?

Common mistakes

  • No loading state — flashing the login page before auth resolves.
  • Always redirecting to the homepage, losing the intended destination.
  • Using push so the protected URL stays in history.
  • Treating the route guard as actual security.
  • Wrapping every route individually instead of a shared layout route.

Performance considerations

  • Resolving auth on every navigation can cause waterfalls — cache the auth state in context/memory so the guard is a synchronous check after the initial resolution.

Edge cases

  • Auth still resolving on first render.
  • Token expires while the user is on a protected page.
  • Deep-linking to a protected route while logged out.
  • Authenticated but lacking the required role.

Real-world examples

  • React Router ProtectedRoute / loader-based redirects.
  • Next.js middleware or layout-level auth checks redirecting to /login.

Senior engineer discussion

Seniors model auth as three states to avoid the login flash, preserve the intended URL, use replace, add role gating and layout-level guards, and stress that guards are UX while the server is the real authorization boundary.

Related questions