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
nextis 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
- Add optimistic updates to a todo list
- Build an accessible modal dialog
- Build infinite scroll with TanStack Query
- Call a cookie-authenticated API with CSRF protection
- Debounce a search input in React
- Handle errors gracefully with error boundaries
- Implement dark mode that follows the system
- Poll a long-running job's status