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.

Where the Check Belongs

In Framework mode every check runs on the server, in middleware, a loader, or an action. A component check is a rendering detail, not a guard, because the loader already sent the data by then.

LayerRunsUse for
middlewareBefore the loaders of that route branchWhole sections behind a login
loaderBefore the route rendersOne route, plus loading the user
actionOn submitEvery write, no exceptions
ComponentIn the browserHiding UI, never for security

Never trust anything the client sends about identity, and re-check in every action. A hidden button is not access control.

Session Storage

createCookieSessionStorage keeps the session in a signed cookie, so it needs no database. Keep it in a .server.ts file so it can never reach the client bundle.

// app/sessions.server.ts
import { createCookieSessionStorage } from "react-router";

type SessionData = { userId: string };
type SessionFlashData = { error: string };

export const { getSession, commitSession, destroySession } =
  createCookieSessionStorage<SessionData, SessionFlashData>({
    cookie: {
      name: "__session",
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
      maxAge: 60 * 60 * 24 * 30,
      secrets: [process.env.SESSION_SECRET!],
    },
  });
Cookie optionWhy
httpOnly: trueJavaScript cannot read it, blunts XSS token theft
secure: trueHTTPS only, off in local dev
sameSite: "lax"Blocks most cross site POSTs, "strict" breaks OAuth returns
path: "/"Sent on every route
maxAgeSeconds. Prefer it over expires, which is fixed at deploy time
secrets: [new, old]Signing keys. First signs, the rest still verify, so keys rotate
Storage helperBacking store
createCookieSessionStorageThe cookie itself, roughly 4 KB limit
createMemorySessionStorageProcess memory, dev and tests only
createFileSessionStorageDisk, Node only
createSessionStorageYour own database, via CRUD callbacks

Reading and Writing a Session

// app/auth.server.ts
import { redirect } from "react-router";
import { getSession } from "./sessions.server";

export async function requireUser(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");
  if (!userId) {
    const url = new URL(request.url);
    const redirectTo = url.pathname + url.search;
    throw redirect(`/login?redirectTo=${encodeURIComponent(redirectTo)}`);
  }
  const user = await db.user.find(userId);
  if (!user) throw redirect("/login");
  return user;
}
// app/routes/dashboard.tsx
export async function loader({ request }: Route.LoaderArgs) {
  const user = await requireUser(request);
  return { user, invoices: await db.invoice.forUser(user.id) };
}

Throwing is what makes this safe. The loader body after the call never runs, so there is no early return to forget.

Session methodDoes
session.get(key)Read a value
session.set(key, value)Write a value
session.has(key)Presence check
session.unset(key)Remove a value
session.flash(key, value)Write a value that is deleted on first read
session.idThe session id, empty for cookie storage

Login and Logout

// app/routes/login.tsx
import { data, redirect } from "react-router";
import { getSession, commitSession } from "../sessions.server";
import type { Route } from "./+types/login";

export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get("Cookie"));
  const form = await request.formData();

  const user = await verifyLogin(
    String(form.get("email")),
    String(form.get("password"))
  );
  if (!user) {
    return data({ error: "Bad credentials" }, { status: 401 });
  }

  session.set("userId", user.id);

  const raw = String(form.get("redirectTo") ?? "/dashboard");
  const to = raw.startsWith("/") && !raw.startsWith("//") ? raw : "/dashboard";

  return redirect(to, {
    headers: { "Set-Cookie": await commitSession(session) },
  });
}
// app/routes/logout.tsx, POST only
export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get("Cookie"));
  return redirect("/", {
    headers: { "Set-Cookie": await destroySession(session) },
  });
}

export function loader() {
  return redirect("/");
}

Logout is a POST action, never a <Link to="/logout">. A GET logout gets triggered by any link prefetch or crawler, and it is trivially CSRF-able.

- Validate redirectTo yourself. A raw ?redirectTo=https://evil.com is an open redirect. - Rotate the session value on privilege changes so an old cookie cannot be replayed. - Set allowedActionOrigins in react-router.config.ts to restrict which origins may submit to your UI route actions.

Passing the User Down

Middleware plus context replaces calling requireUser in every child loader of a protected section.

// app/routes/dashboard/layout.tsx
import { createContext, redirect } from "react-router";
import type { Route } from "./+types/layout";

export const userContext = createContext<User>();

export const middleware: Route.MiddlewareFunction[] = [
  async ({ request, context }, next) => {
    const session = await getSession(request.headers.get("Cookie"));
    const user = await db.user.find(session.get("userId"));
    if (!user) throw redirect("/login");
    context.set(userContext, user);
    return next();
  },
];
// any child route in that branch
export async function loader({ context }: Route.LoaderArgs) {
  const user = context.get(userContext);
  return { invoices: await db.invoice.forUser(user.id) };
}

For rendering, put the user on a root loader and read it with useRouteLoaderData("root"), so the nav knows who is signed in without a second request.