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

In this guide, you add React Router to the app from Getting started, in the
mode your app uses. React Router has three modes, and Session kit guards each
one the way that mode works:

| Mode                         | The guard                                    | Beside it                              |
| ---------------------------- | -------------------------------------------- | -------------------------------------- |
| Declarative                  | `SessionOutlet`                              | Nothing                                |
| Data                         | `requireSessionLoader` as the `loader`       | `SessionOutlet`, `useAuthRevalidation` |
| Framework, with `ssr: false` | `requireSessionLoader` as the `clientLoader` | `SessionOutlet`, `useAuthRevalidation` |

Each mode has a complete example app. For framework mode with server
rendering, follow
[React Router with server rendering](https://session-kit.pages.dev/docs/setups/react-router-ssr) instead.

## Before you begin

Follow [Getting started](https://session-kit.pages.dev/docs/getting-started) through step 3. This guide
uses its client in `src/auth.ts` and its form in `src/sign-in.tsx`.

## Installation

To install the packages, run the following command:

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

React Router 7 and 8 both work.

## Add a page to protect

In declarative and data mode, `AuthProvider` goes around the router. In
framework mode, it goes in the root route. It mounts the client in a layout
effect, so with `localStorage` and a synchronous schema, the session is
restored before the first paint. Nothing starts on a server. Register the
client where you render it, as step 4 of Getting started shows, and the hooks
from `@monarcode/session-kit/react` carry your user.

Every mode on this page protects the same page:

```tsx title="src/home.tsx"
import { useAuth, useAuthClient } from "@monarcode/session-kit/react";

export function Home() {
  const user = useAuth((state) => state.user);
  const auth = useAuthClient();
  return (
    <main>
      <h1>Welcome, {user?.firstName}</h1>
      <button onClick={() => void auth.signOut().catch(() => {})}>
        Sign out
      </button>
    </main>
  );
}
```

## Declarative mode

Declarative mode has no loaders, so `SessionOutlet` is the guard. Render it
as a layout route's element, `<Route element={<Private />}>`, and the routes
inside it render only while someone is signed in:

```tsx title="src/main.tsx"
import { StrictMode } from "react";
import {
  BrowserRouter,
  Navigate,
  Route,
  Routes,
  useSearchParams,
} from "react-router";

import { isSignedIn, safeReturnTo } from "@monarcode/session-kit";
import { AuthProvider, useAuth } from "@monarcode/session-kit/react";
import { SessionOutlet } from "@monarcode/session-kit/react-router";
import { createRoot } from "react-dom/client";

import { auth } from "./auth.js";
import { Home } from "./home.js";
import { SignIn } from "./sign-in.js";

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

/** The guard: its child routes render only while someone is signed in. */
function Private() {
  return (
    <SessionOutlet
      pending={<p>Checking session…</p>}
      unavailable={({ error, retry }) => (
        <div role="alert">
          <p>{error?.message ?? "Can't reach the server."}</p>
          <button onClick={retry}>Try again</button>
        </div>
      )}
    />
  );
}

/** Signing in changes the state, which renders this again and redirects. */
function Login() {
  const signedIn = useAuth(isSignedIn);
  const [search] = useSearchParams();
  if (signedIn)
    return <Navigate to={safeReturnTo(search.get("redirectTo"))} replace />;
  return <SignIn />;
}

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider client={auth}>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route element={<Private />}>
            <Route index element={<Home />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  </StrictMode>,
);
```

`SessionOutlet` shows `pending` while the saved session is restored, sends
signed-out users to `loginPath` (`/login` unless you pass another) with
`redirectTo` set to where they were going, and renders `unavailable` after a
failed refresh. Without `unavailable`, the error is thrown to the nearest
error boundary.

`safeReturnTo` accepts only same-origin paths and returns `/` for anything
else, including the sign-in page itself, so a stale `redirectTo` can't loop.

A complete app is in
[`examples/react-router-declarative`](https://github.com/monarcode/session-kit/tree/main/examples/react-router-declarative).

## Data mode

In data mode, loaders run before a route renders, so the guard goes there. A
signed-out user is redirected before a private route renders:

```tsx title="src/router.tsx"
import { createBrowserRouter, Outlet, useRouteError } from "react-router";

import { useAuthClient } from "@monarcode/session-kit/react";
import {
  redirectIfSignedInLoader,
  requireSessionLoader,
  SessionOutlet,
  useAuthRevalidation,
} from "@monarcode/session-kit/react-router";

import { auth } from "./auth.js";
import { Home } from "./home.js";
import { SignIn } from "./sign-in.js";

/** Runs the loaders again when auth changes, and with them the guards. */
function Root() {
  useAuthRevalidation();
  return <Outlet />;
}

/** Shown after a failed refresh; a repeated failure shows here again. */
function SessionError() {
  const error = useRouteError();
  const client = useAuthClient();
  return (
    <div role="alert">
      <p>{error instanceof Error ? error.message : "Something went wrong"}</p>
      <button onClick={() => void client.retry().catch(() => {})}>Retry</button>
    </div>
  );
}

export const router = createBrowserRouter([
  {
    element: <Root />,
    HydrateFallback: () => <p>Checking session…</p>,
    children: [
      {
        path: "/login",
        loader: redirectIfSignedInLoader(auth),
        element: <SignIn />,
      },
      {
        loader: requireSessionLoader(auth),
        errorElement: <SessionError />,
        element: <SessionOutlet pending={<p>Checking session…</p>} />,
        children: [{ index: true, element: <Home /> }],
      },
    ],
  },
]);
```

Then render the router inside the provider, in `src/main.tsx`:

```tsx
import { StrictMode } from "react";
import { RouterProvider } from "react-router/dom";

import { AuthProvider } from "@monarcode/session-kit/react";
import { createRoot } from "react-dom/client";

import { auth } from "./auth.js";
import { router } from "./router.js";

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

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider client={auth}>
      <RouterProvider router={router} />
    </AuthProvider>
  </StrictMode>,
);
```

Three pieces work together:

- **`requireSessionLoader(auth)`** is the route's loader. It redirects
  signed-out users to `/login` with `redirectTo`, and returns `{ session }`
  as the route's loader data. Pass `{ loginPath }` for another sign-in page.
  A failed refresh is rethrown for the route's error element instead,
  because the user is still signed in. A loader that does more calls
  `requireSession` instead, as [Guards in loaders](#guards-in-loaders)
  shows.
- **`useAuthRevalidation`** runs the loaders again after a sign-in, a
  sign-out, an account switch, or a profile update, so the guards run again.
  Call it once, in the root route.
- **`SessionOutlet`**, as the guarded route's element, renders child routes
  only while the session the loader accepted is still the signed-in one. When
  another account signs in, it shows `pending` until the loader has run for
  the new session, so no page sees the previous account's loader data.

A complete app is in [`examples/react-router-data`](https://github.com/monarcode/session-kit/tree/main/examples/react-router-data).

## Framework mode with `ssr: false`

Framework mode without a server is data mode with route modules. The guards
are the same, in `clientLoader`. Keep the files from Getting started, and
`home.tsx` from [Add a page to protect](#add-a-page-to-protect), in `app/`.
Framework mode also needs `@react-router/dev`, which a framework-mode app
already has.

Turn server rendering off:

```ts
// react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default { ssr: false } satisfies Config;
```

Provide the client, and watch for auth changes, in the root route:

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

import { AuthProvider } from "@monarcode/session-kit/react";
import { useAuthRevalidation } from "@monarcode/session-kit/react-router";

import { auth } from "./auth.js";

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

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

/** Runs the client loaders again when auth changes, and with them the guards. */
function Revalidate() {
  useAuthRevalidation();
  return <Outlet />;
}

export default function App() {
  return (
    <AuthProvider client={auth}>
      <Revalidate />
    </AuthProvider>
  );
}

/** Prerendered into index.html, and shown until the client loaders finish. */
export function HydrateFallback() {
  return <p>Checking session…</p>;
}
```

Guard a layout route, and nest the private routes under it:

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

import { useAuthClient } from "@monarcode/session-kit/react";
import {
  requireSessionLoader,
  SessionOutlet,
} from "@monarcode/session-kit/react-router";

import { auth } from "../auth.js";

export const clientLoader = requireSessionLoader(auth);

export default function Protected() {
  return <SessionOutlet pending={<p>Checking session…</p>} />;
}

/** Shown after a failed refresh; a repeated failure shows here again. */
export function ErrorBoundary() {
  const error = useRouteError();
  const client = useAuthClient();
  return (
    <div role="alert">
      <p>{error instanceof Error ? error.message : "Something went wrong"}</p>
      <button onClick={() => void client.retry().catch(() => {})}>Retry</button>
    </div>
  );
}
```

Add the sign-in route, which sends signed-in users on to `redirectTo`. Its
loader runs again after a sign-in, so the form doesn't navigate:

```tsx title="app/routes/login.tsx"
import { redirectIfSignedInLoader } from "@monarcode/session-kit/react-router";

import { auth } from "../auth.js";
import { SignIn } from "../sign-in.js";

export const clientLoader = redirectIfSignedInLoader(auth);

export default SignIn;
```

In `app/routes.ts`, nest the routes the layout protects:

```ts
import {
  index,
  layout,
  route,
  type RouteConfig,
} from "@react-router/dev/routes";

export default [
  route("login", "routes/login.tsx"),
  layout("routes/protected.tsx", [index("routes/home.tsx")]),
] satisfies RouteConfig;
```

`routes/home.tsx` is the page, as its default export:
`export { Home as default } from "../home.js";`.

`AuthProvider` mounts the client in an effect, so the build-time prerender of
the root route starts nothing, and the browser restores the session.

A complete app is in
[`examples/react-router-framework-spa`](https://github.com/monarcode/session-kit/tree/main/examples/react-router-framework-spa).

## Guards in loaders

Two functions guard a loader, in data mode and in framework mode. Both send a
signed-out user to `/login`, with `redirectTo`, and resolve `{ session }`,
typed with your user:

- **`requireSessionLoader(auth)`** is the whole loader. Use it on a route
  that guards and loads nothing else, usually the layout route around your
  private pages, as in [Data mode](#data-mode).
- **`requireSession(auth, { request })`** is a call inside a loader of your
  own. Use it when the loader loads data too: data only a signed-in user may
  load, or data that needs who is signed in.

React Router runs the loaders of nested routes at the same time, so a page's
loader doesn't wait for its layout's guard. A page whose loader calls your
API checks the session itself:

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

import { requireSession } from "@monarcode/session-kit/react-router";

import { fetchProfile } from "../api.js";
import { auth } from "../auth.js";

export async function clientLoader({ request }: { request: Request }) {
  const { session } = await requireSession(auth, { request });
  return { name: session.user.firstName, profile: await fetchProfile() };
}

export default function Profile() {
  const { name, profile } = useLoaderData<typeof clientLoader>();
  return (
    <p>
      {name}, your email is {profile.email}.
    </p>
  );
}
```

A signed-out user is redirected before your API is called, rather than
after a request that fails. Pass `requireSession` the same `loginPath` and
`basename` as the layout's guard.

The sign-in page has the same pair: `redirectIfSignedInLoader(auth)` as its
loader, or `redirectIfSignedIn(auth, { redirectTo })` in a loader that does
more.

## Layouts around guarded pages

A guarded route often has UI of its own around its pages, such as a sidebar
and a top bar. Render it in the guarded route's element, with `SessionOutlet`
where the pages go:

```tsx title="src/dashboard.tsx"
import { useAuth } from "@monarcode/session-kit/react";
import { SessionOutlet } from "@monarcode/session-kit/react-router";

/** The guarded route's element: `element: <Dashboard />`. */
export function Dashboard() {
  // From state, not loader data: this stays mounted through a sign-out.
  const user = useAuth((state) => state.user);
  const sessionId = useAuth((state) => state.sessionId);
  return (
    <div className="dashboard">
      <header>{user ? `Signed in as ${user.firstName}` : null}</header>
      {/* Keyed by session, so one account's choices never show for the next. */}
      <nav key={sessionId}>…</nav>
      <main>
        <SessionOutlet pending={<p>Checking session…</p>} />
      </main>
    </div>
  );
}
```

Three things follow from where `SessionOutlet` sits:

- **It protects only what it renders.** After a sign-out it redirects to the
  sign-in page. After an account switch it remounts the pages, and in data
  and framework mode shows `pending` until the guard runs again. The layout
  around it stays mounted meanwhile.
- **Read the user from state there.** In the layout, use
  `useAuth((state) => state.user)` and handle `null`. The loader's `session`
  is the one the guard accepted, which for that moment is the previous one.
- **Key what belongs to an account.** Layout UI that holds state for one
  account, such as a draft or an open panel, takes the session as its `key`,
  so the next account starts fresh.

## Things to know

- **Another sign-in page.** If sign-in isn't at `/login`, pass the same
  `loginPath` to `SessionOutlet`, `requireSessionLoader` (or
  `requireSession`), `redirectIfSignedInLoader` and `safeReturnTo`.
- **A router `basename`.** Pass it to `requireSessionLoader` or
  `requireSession` too, so `redirectTo` leaves it out. Redirects add it
  again. `SessionOutlet` needs nothing.
- **A failed refresh is not a redirect.** In declarative mode,
  `SessionOutlet` renders `unavailable`, or throws to the nearest error
  boundary without it. In data and framework mode, the guard rethrows it for
  the route's error element, which can offer `auth.retry()`.
- **Locked sessions.** [Locking](https://session-kit.pages.dev/docs/client/locking) is for native apps, so
  the guards treat a locked session as signed out.
- **Cached data.** `state.sessionId` is new on every sign-in. Scope private
  cached data, such as TanStack Query results, to it.

## Next steps

- [Call your API](https://session-kit.pages.dev/docs/getting-started#5-call-your-api) with `authFetch`, as
  in Getting started, or with
  [ky, axios or openapi-fetch](https://session-kit.pages.dev/docs/client/other-http-clients). It works
  unchanged with the router.
- [Using the client](https://session-kit.pages.dev/docs/client): updating the user, signing out, and the
  client's other methods.
- [Effect and effetch](https://session-kit.pages.dev/docs/effect): the same client as an Effect service.