> ## Documentation Index
> Fetch the complete documentation index at: https://better-router.bansal.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SSR, hydration, and SPA

> How the first request, client hydration, and client-side navigation work together.

better-router defaults to server rendering plus client hydration, then SPA navigation.

## First request

1. Hono matches the route.
2. Layout loaders run outside-in, then the page loader.
3. React renders HTML with `renderToString`.
4. Loader data is written to `window.__BR_DATA__`.
5. The browser hydrates `#app`.

The HTML document comes from `src/routes/_document.tsx` or the default shell. Client navigations leave that shell in place and only swap `#app`. See [Layouts](/layouts#document).

## Later navigation

`<Link href="/about">` intercepts the click (plain left click, no modifier keys), requests JSON with `x-better-router-navigate: 1`, and swaps the page component.

```tsx theme={null}
import { Link, useRouter } from "better-router/react";

<Link href="/projects/1">Open</Link>
<Link href="/logout" reloadDocument>Full load</Link>
<Link href="/projects?tab=done" replace>Replace history</Link>
```

Programmatic navigation:

```tsx theme={null}
const router = useRouter();
router.visit("/users");
router.visit("/users", { replace: true });
router.refresh(); // re-run loaders for the current URL
router.navigating; // true while a fetch is in flight
router.path;
```

`<Form>` uses the same JSON channel. Island pages and `spa = false` routes do a full document load instead.

## Modes

| Mode      | How you opt in                                                     | Behavior                                     |
| --------- | ------------------------------------------------------------------ | -------------------------------------------- |
| SSR       | default                                                            | Server HTML, hydrate, then SPA               |
| SPA shell | `export const ssr = false` in `.server.tsx`                        | Empty `#app`, client render with loader data |
| Island    | `*.island.tsx`                                                     | Static HTML, hydrate marked islands only     |
| Document  | `export const spa = false`, `reloadDocument`, or island navigation | Full page load                               |

Island pages and regular pages can live in the same app. Navigating **to** an island page does a full load because there is no SPA payload for that route.

## CSS and client-only code

Import CSS from a layout or document. Vite injects it in dev; the client build emits hashed assets.

```tsx theme={null}
import "../app.css";
```

Guard browser-only APIs:

```tsx theme={null}
import { useEffect, useState } from "react";

export default function Chart() {
  const [ready, setReady] = useState(false);
  useEffect(() => setReady(true), []);
  if (!ready) return null;
  return <canvas />;
}
```

Do not import Node modules or Worker bindings helpers from a `.tsx` page. Put that work in `.server.tsx`.

## Payload

`window.__BR_DATA__` is the SPA payload:

```ts theme={null}
{
  url: string
  path: string
  params: Record<string, string>
  query: Record<string, string | string[]>
  props: unknown          // page loader return
  layoutProps: unknown[]  // each layout loader, outside-in
  mode: "ssr" | "spa" | "island"
  ssr: boolean
  routeId: string
}
```

`useLoaderData()` reads `props`. Layouts receive their own `layoutProps[i]` as React props, not through the hook.
