React Router Cheatsheet

Auth Patterns

Use this React Router reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Auth Patterns

In data mode, gate in the loader rather than in a component effect. The loader runs before render, so there is no flash of protected UI.

// A shared loader guard
async function requireUser(request) {
  const user = await getUserFromSession(request);
  if (!user) {
    const url = new URL(request.url);
    throw redirect(`/login?next=${encodeURIComponent(url.pathname + url.search)}`);
  }
  return user;
}

const router = createBrowserRouter([
  {
    path: "/dashboard",
    element: <DashboardLayout />,
    loader: async ({ request }) => {
      const user = await requireUser(request);
      return { user };
    },
    children: [
      { index: true, element: <Overview />, loader: overviewLoader },
    ],
  },
]);

Because the parent's loader throws a redirect before the children render, one guard on a layout route protects the whole subtree.

// The login action, honoring ?next=
export async function action({ request }) {
  const form = await request.formData();
  const user = await verify(form.get("email"), form.get("password"));
  if (!user) return data({ error: "Invalid credentials" }, { status: 401 });

  const url = new URL(request.url);
  const next = url.searchParams.get("next") ?? "/dashboard";
  // Only allow same-origin, path-only redirects
  const safe = next.startsWith("/") && !next.startsWith("//") ? next : "/dashboard";
  return redirect(safe, { headers: { "Set-Cookie": await commitSession(user) } });
}

That safe check matters. Redirecting to an unvalidated ?next= value is an open redirect, and it is one of the most common vulnerabilities in login flows.

Declarative Mode Guard

Without a data router there are no loaders, so guard during render.

function RequireAuth({ children }) {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) return <Spinner />;
  if (!user) return <Navigate to="/login" replace state={{ from: location }} />;
  return children;
}

<Route
  path="/dashboard"
  element={<RequireAuth><DashboardLayout /></RequireAuth>}
>
  <Route index element={<Overview />} />
</Route>

The loading check is essential. Without it, the first render sees user === null while the session is still being checked and bounces an authenticated user to the login page.

Role-Based Access

function RequireRole({ role, children }) {
  const user = useRouteLoaderData("root")?.user;
  if (!user) return <Navigate to="/login" replace />;
  if (!user.roles.includes(role)) throw new Response("Forbidden", { status: 403 });
  return children;
}

Client-side checks are UI, not security. Every protected loader and action must re-verify authorization on the server, because a determined user can call your endpoints directly.

Handling Session Expiry

// In a loader, when the API says the session is gone
const res = await fetch("/api/data", { headers: authHeaders() });
if (res.status === 401) throw redirect("/login?expired=1");
if (!res.ok) throw new Response("Upstream error", { status: 502 });
return res.json();

Logout

// A logout must be a POST, never a GET link
export async function action({ request }) {
  return redirect("/", { headers: { "Set-Cookie": await destroySession(request) } });
}

function LogoutButton() {
  return (
    <Form method="post" action="/logout">
      <button>Sign out</button>
    </Form>
  );
}

A <Link to="/logout"> is a GET, and browsers, extensions, and link prefetchers all follow GETs speculatively. That is how users get randomly signed out.

Sharing the Current User

// Load it once in the root loader
{ id: "root", path: "/", loader: async ({ request }) => ({
    user: await getUserFromSession(request),
  }), children: [...] }
// Read it anywhere, no context provider needed
function Header() {
  const { user } = useRouteLoaderData("root");
  return user ? <Avatar user={user} /> : <Link to="/login">Sign in</Link>;
}

Because the root loader revalidates after every action, signing in or out updates the header automatically.

Auth Summary

NeedData modeDeclarative mode
Gate a subtreeloader on the layout route, throw redirect()A <RequireAuth> wrapper element
Read the useruseRouteLoaderData("root")Your own context
Sign inaction + Set-Cookie + redirectCall your API, then navigate
Sign out<Form method="post"> to a logout actionPOST, then navigate
Refresh after auth changeAutomatic revalidationRefetch by hand
Authorize a mutationRe-check in the action, alwaysCheck on your server, always