# React Router with server rendering (https://session-kit.pages.dev/docs/setups/react-router-ssr)

In this guide, you keep the session in an encrypted cookie in React Router's
framework mode. An action signs in, a session route answers the browser,
middleware refreshes the tokens, and loaders read the session. The first
response already shows the signed-in page, so the browser never shows a
"Checking session…" state or a flash of signed-out content. Components get a
small client that mirrors the session, and the
[hooks](https://session-kit.pages.dev/docs/getting-started#4-read-the-session-in-react) work with it.
[Server rendering](https://session-kit.pages.dev/docs/server-rendering) explains how the pieces fit.

For framework mode with `ssr: false`, follow
[React Router](https://session-kit.pages.dev/docs/setups/react-router#framework-mode-with-ssr-false)
instead. A complete app is in
[`examples/react-router-framework-ssr`](https://github.com/monarcode/session-kit/tree/main/examples/react-router-framework-ssr).

## Before you begin

- React Router's middleware: React Router 8, or 7.9 and later with
  `future.v8_middleware`. The code on this page is for React Router 8. On
  7.14 and earlier, read the URL with `new URL(request.url)` where the code
  uses the `url` argument.
- A secret of 32 or more random characters to seal the cookie, set as
  `SESSION_SECRET` on the server. To make one, run
  `openssl rand -base64 32`. `react-router build` doesn't need it.

## Installation

To install the packages, run the following command:

```npm
npm install @monarcode/session-kit react-router zod
```

## 1. Create the server engine

`createServerAuth` from `@monarcode/session-kit/server` keeps the session in
a cookie sealed with your `secret`, and refreshes it with your backend.
`routerAuth` gives a request's session by its `context`, and
`requireSession` makes the guard that protects routes in step 4. Create them
in a `.server` module, so React Router refuses to bundle it for the browser:

```ts title="app/auth/session.server.ts"
import {
  requireSession,
  routerAuth,
} from "@monarcode/session-kit/react-router/server";
import { createServerAuth } from "@monarcode/session-kit/server";
import { z } from "zod";

const API = "https://api.example.com";

const userSchema = z.object({ id: z.number(), firstName: z.string() });
const tokens = z.object({ accessToken: z.string(), refreshToken: z.string() });

export const serverAuth = createServerAuth({
  name: "app",
  user: userSchema,
  secret: process.env.SESSION_SECRET!,
  refresh: async ({ refreshToken, signal }) => {
    const response = await fetch(`${API}/auth/refresh`, {
      method: "POST",
      signal,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken }),
    });
    // The backend rejected the refresh token: sign out.
    if (response.status === 401) return null;
    if (!response.ok) throw new Error(`Refresh failed (${response.status})`);
    return tokens.parse(await response.json());
  },
});

/** The request's session: `auth(context)` in middleware, loaders and actions. */
export const auth = routerAuth(serverAuth);

/** Lets only signed-in users through to the routes it guards. */
export const signedIn = requireSession(serverAuth);

/** Signs in with your backend, from the server. */
export async function login(username: string, password: string) {
  const response = await fetch(`${API}/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });
  const body: unknown = await response.json().catch(() => null);
  if (!response.ok) {
    // The backend says why, such as wrong credentials, in `message`.
    const failure = z.object({ message: z.string() }).safeParse(body);
    throw new Error(
      failure.success
        ? failure.data.message
        : `Sign-in failed (${response.status})`,
    );
  }
  return { ...tokens.parse(body), user: userSchema.parse(body) };
}
```

[Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions#the-cookie) covers
its other options, rotating the secret, signing out, and the limits every
setup shares.

## 2. Add the root route

The root route's middleware is `sessionMiddleware(serverAuth)` from
`@monarcode/session-kit/react-router/server`. It reads each request's
cookies and sets the ones written on the response, redirects included, so
`auth(context)` gives the request's session in middleware, loaders and
actions. Then it refreshes access that is about to expire, before any loader
reads it, and marks each response that doesn't set its own
`Cache-Control` as `private, no-store`, so shared caches keep none. A failed
refresh doesn't fail the request. Loaders read the session as usual, and an
error there reaches the route's error boundary.

`createMirrorAuth` makes the client that components use, from the session
the root loader read and the session route that step 3 adds:

```ts title="app/auth/client.ts"
import { createMirrorAuth, type SessionOf } from "@monarcode/session-kit";

// Types only: left out of the browser's bundle.
import type { serverAuth } from "./session.server";

export type AppSession = SessionOf<typeof serverAuth>;

export function createAppAuth(session: AppSession | null) {
  return createMirrorAuth({
    name: "app",
    session,
    endpoint: "/auth/session",
  });
}

export type AppAuth = ReturnType<typeof createAppAuth>;
```

The root route runs the middleware and reads the session, and its component
creates the client with `useMirrorAuth` from `@monarcode/session-kit/react`:
once per request on the server, never at module level. It keeps the client
and the server in step both ways. When the session changes in the browser,
it calls `onChange`, here `revalidate` from `useRevalidator()`, which runs
the loaders again. Register the client's type here, where you render
`AuthProvider`, so the hooks carry your user:

```tsx title="app/root.tsx"
import type { ReactNode } from "react";
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  useLoaderData,
  useRevalidator,
  type LoaderFunctionArgs,
} from "react-router";

import { AuthProvider, useMirrorAuth } from "@monarcode/session-kit/react";
import { sessionMiddleware } from "@monarcode/session-kit/react-router/server";

import { createAppAuth, type AppAuth } from "./auth/client";
import { auth, serverAuth } from "./auth/session.server";

// Types `AuthProvider` and the hooks with this client.
declare module "@monarcode/session-kit" {
  interface Register {
    auth: AppAuth;
  }
}

export const middleware = sessionMiddleware(serverAuth);

export async function loader({ context }: LoaderFunctionArgs) {
  return { session: await auth(context).getSession() };
}

export function Layout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  const { session } = useLoaderData<typeof loader>();
  const { revalidate } = useRevalidator();
  const auth = useMirrorAuth({
    create: createAppAuth,
    session,
    onChange: revalidate,
  });
  return (
    <AuthProvider client={auth}>
      <Outlet />
    </AuthProvider>
  );
}
```

`getSession` resolves `{ sessionId, user }`, never tokens, so it's safe to
return from a loader. After an action signs in or out, the loader reads the
new session and the client adopts it. After a sign-out in the browser, here
or in another tab, `onChange` runs the loaders again, so the server's
redirects run. The client-side guides add `useAuthRevalidation` for this.
Here, `useMirrorAuth` does it.

## 3. Sign in and out

The sign-in route's action signs in with your backend and seals the result
into the cookie. React Router 7.12 and later refuse route actions posted from
other sites. On 7.9 to 7.11, refuse them yourself at the start of the
action, as [The session route](https://session-kit.pages.dev/docs/security#the-session-route) explains.
The form works before the page's scripts load:

```tsx title="app/routes/login.tsx"
import {
  data,
  Form,
  redirect,
  useActionData,
  type ActionFunctionArgs,
} from "react-router";

import { safeReturnTo } from "@monarcode/session-kit";
import { redirectIfSignedIn } from "@monarcode/session-kit/react-router/server";

import { auth, login, serverAuth } from "../auth/session.server";

// A signed-in user goes on to `redirectTo`.
export const loader = redirectIfSignedIn(serverAuth);

export async function action({ request, context, url }: ActionFunctionArgs) {
  const form = await request.formData();
  try {
    const tokens = await login(
      String(form.get("username")),
      String(form.get("password")),
    );
    await auth(context).signIn(tokens);
  } catch (error) {
    return data(
      { error: error instanceof Error ? error.message : "Sign-in failed" },
      { status: 400 },
    );
  }
  throw redirect(safeReturnTo(url.searchParams.get("redirectTo")));
}

export default function Login() {
  const result = useActionData<typeof action>();
  return (
    <Form method="post">
      <label>
        Username
        <input name="username" autoComplete="username" />
      </label>
      <label>
        Password
        <input
          name="password"
          type="password"
          autoComplete="current-password"
        />
      </label>
      {result?.error && <p role="alert">{result.error}</p>}
      <button>Sign in</button>
    </Form>
  );
}
```

A failed sign-in returns its message with status 400 for the form to show.
React Router doesn't run the page's loaders again after a 4xx action.

The browser's client reads the session and signs out through a session
route, a resource route. `sessionRoute` from
`@monarcode/session-kit/react-router/server` makes its loader and action:

```ts title="app/routes/auth.session.ts"
import { sessionRoute } from "@monarcode/session-kit/react-router/server";

import { serverAuth } from "../auth/session.server";

const route = sessionRoute(serverAuth);

export const loader = route.loader;
export const action = route.action;
```

Add it to `app/routes.ts` as `route("auth/session", "routes/auth.session.ts")`.
Export the loader and action one by one, as `app/routes/auth.session.ts`
does. React Router's build fails on a destructured
`export const { loader, action }`. The route answers your
own site's pages only, so another site can't sign the user out: see
[Security](https://session-kit.pages.dev/docs/security).

## 4. Protect routes

Guard a layout route with `signedIn`, the middleware `requireSession` made.
It runs on the server before any loader or action nested in it, which
loaders on their own can't promise. Signed-out users go to `/login`, with
`?redirectTo=` set to the page they asked for:

```tsx title="app/routes/protected.tsx"
import { Outlet } from "react-router";

import { signedIn } from "../auth/session.server";

export const middleware = [signedIn];

// So navigation in the browser runs the middleware.
export const loader = () => null;

export default function Protected() {
  return <Outlet />;
}
```

A loader nested in it gets the session from `signedIn.session(context)`,
which is never `null` there. Components read the user with the hooks as
usual:

```tsx title="app/routes/home.tsx"
import { useLoaderData, type LoaderFunctionArgs } from "react-router";

import { signedIn } from "../auth/session.server";

export function loader({ context }: LoaderFunctionArgs) {
  return { name: signedIn.session(context).user.firstName };
}

export default function Home() {
  const { name } = useLoaderData<typeof loader>();
  return <h1>Welcome, {name}</h1>;
}
```

In `app/routes.ts`, nest the routes it protects:
`layout("routes/protected.tsx", [index("routes/home.tsx")])`. During
navigation in the browser, server middleware runs only when a route has a
loader to fetch, so the layout has one in case the routes nested in it don't.

In this guide, the server signs in with your backend, so the user in the cookie
comes from the backend. If the browser signs in with the backend itself, have
the action take the tokens instead, as
[Hybrid setup](https://session-kit.pages.dev/docs/server-rendering/hybrid) shows for TanStack Start.

## Call your API

Call your API with `createAuthFetch` from `@monarcode/session-kit/http`.
Given the request's session, it sends the access token, refreshed first when
it has expired. After a 401, it renews the token, saves it in the cookie, and
sends a `GET` again:

```tsx title="app/routes/orders.tsx"
import { redirect, useLoaderData, type LoaderFunctionArgs } from "react-router";

import { loginHref } from "@monarcode/session-kit";
import { createAuthFetch } from "@monarcode/session-kit/http";
import { z } from "zod";

import { auth } from "../auth/session.server";

export async function loader({ context }: LoaderFunctionArgs) {
  const session = auth(context);
  // Outside the guarded layout: send the user to sign in, then back here.
  if (!(await session.getSession()))
    throw redirect(loginHref("/login", "/orders"));
  const apiFetch = createAuthFetch(session, {
    baseUrl: "https://api.example.com/",
  });
  const response = await apiFetch("orders");
  if (!response.ok) throw new Error(`Orders failed (${response.status})`);
  return z.array(z.object({ id: z.number() })).parse(await response.json());
}

export default function Orders() {
  const orders = useLoaderData<typeof loader>();
  return <p>{orders.length} orders</p>;
}
```

With an HTTP client of your own, such as ky or axios, see
[Other HTTP clients](https://session-kit.pages.dev/docs/client/other-http-clients). A client without a
`fetch` option reads the token with `credentials.get()`.

To call the API from the browser too, with `createAuthFetch` or effetch's
`sessionBearer`, opt in with `clientCredentials: true` in `createServerAuth`.
The session route then serves the access token, and the client's
`credentials` get it there. The browser keeps the access token in memory,
never the refresh token; see
[Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions#the-access-token-in-the-browser).
The example app does this.

## Things to know

- **Signing out ends the session only with `revoke`.** Without it, a copy of
  the cookie made before sign-out still works: see
  [Sign-out](https://session-kit.pages.dev/docs/server-rendering/server-sessions#sign-out).
- **Refresh in middleware.** The request's session sets cookies until the
  response leaves `sessionMiddleware`, which first waits for a refresh under
  way. A loader's deferred data that reads the session later, while the
  page streams, can't set cookies: an expired token then rejects with
  `REFRESH_FAILED`, since nothing new can be saved. `sessionMiddleware`
  refreshes before that.
- **One session per request.** `auth(context)` returns the same session for
  every call with the same `context`, which React Router makes per request.
- **Caching.** `sessionMiddleware` leaves a response that sets its own
  `Cache-Control` as it is. Pass `cacheControl` to send another value, or
  `{ cacheControl: false }` to leave every response as it is.
- **Guards with the same names.** `requireSession` and `redirectIfSignedIn`
  on this page come from `@monarcode/session-kit/react-router/server`.
  `@monarcode/session-kit/react-router` exports guards with the same names
  that take the browser's client, for apps without server rendering: see
  [React Router](https://session-kit.pages.dev/docs/setups/react-router).
- **A `basename`.** Pass it to the guard too, as
  `requireSession(serverAuth, { basename: "/app" })`. React Router adds it to
  loaders' and actions' redirects, but not to middleware's, so the guard adds
  it to its own. The client's `endpoint` includes it as well:
  `"/app/auth/session"`.
- **Tabs.** Signing in or out in one tab reaches the others, which ask the
  server again and run their loaders again. So does returning to a tab after
  a while.
- **More.** Several server instances, the cookie's size, and plain http
  during development: see
  [Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions).

## Next steps

- [Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions): the cookie,
  signing out, and the limits every server-rendered setup shares.
- [Using the client](https://session-kit.pages.dev/docs/client): what the browser client can do.
- [Effect on the server](https://session-kit.pages.dev/docs/effect#on-the-server): run Effects with a
  request's session.