React cookbook · Intermediate

Protect routes in a React SPA

Show some pages only to signed-in users (and some only to admins), redirecting others to login and back.

Approach

Load the current user once (e.g., /api/me via TanStack Query), then wrap protected route groups in a guard layout that shows a loader while checking, redirects to /login?next=... when signed out, and renders <Outlet /> otherwise. Remember: route guards are UX only — the API must enforce auth on every request.

Solution

import { Navigate, Outlet, useLocation } from "react-router";
import { useQuery } from "@tanstack/react-query";

function useCurrentUser() {
  return useQuery({
    queryKey: ["me"],
    queryFn: async () => {
      const res = await fetch("/api/me", { credentials: "include" });
      if (res.status === 401) return null;
      if (!res.ok) throw new Error("Failed to load session");
      return (await res.json()) as { id: string; name: string; roles: string[] };
    },
    staleTime: 5 * 60_000,
  });
}

export function RequireAuth({ role }: { role?: string }) {
  const { data: user, isPending } = useCurrentUser();
  const location = useLocation();

  if (isPending) return <p aria-busy="true">Checking your session…</p>;
  if (!user) return <Navigate to={`/login?next=${encodeURIComponent(location.pathname + location.search)}`} replace />;
  if (role && !user.roles.includes(role)) return <Navigate to="/forbidden" replace />;
  return <Outlet />;
}

// Routes
// { element: <RequireAuth />, children: [{ path: "/app", element: <Dashboard /> }] },
// { element: <RequireAuth role="admin" />, children: [{ path: "/admin", element: <Admin /> }] },

Pitfalls

  • Treating the client guard as security.
  • Open redirects: validate next is a relative path before navigating after login.
  • Flashing protected content before the auth check completes.

Variations

  • React Router data mode: check auth in a parent route loader and throw redirect().
  • Next.js: protect routes in middleware/proxy plus server-side checks.

Further reading: React Router: route loaders

Reference: https://reactrouter.com/start/data/route-object#loader

More React recipes