# How React Router Supports Server Side Rendering (SSR): The Complete Guide

> Learn how React Router supports Server Side Rendering pre-executing data loaders on the server and hydrating client-side for faster performance. Get the complete guide.

- Repository: [Remix/react-router](https://github.com/remix-run/react-router)
- Tags: deep-dive
- Published: 2026-03-06

---

**React Router enables Server Side Rendering by using a static handler to pre-execute data loaders on the server, creating a read-only router instance that renders to HTML, and seamlessly hydrating the application on the client via `StaticRouterProvider`.**

React Router's Server Side Rendering (SSR) architecture allows applications to pre-render route components on the server while executing data loaders before sending HTML to the browser. The `remix-run/react-router` repository implements this through a dedicated server-side API that mirrors client-side routing behavior without requiring a browser environment.

## How React Router SSR Works: The Core Architecture

React Router implements SSR through four distinct phases that separate data loading from rendering. This design ensures that all asynchronous data fetching completes before HTML generation begins.

### Step 1: Create the Static Handler

The process begins with `createStaticHandler`, defined in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts) (lines 3733-3760). This function transforms route definitions into a data-only structure capable of executing loaders and actions without browser APIs.

```typescript
import { createStaticHandler } from "@react-router/server";

const handler = createStaticHandler(routes);

```

The handler provides a `query(request)` method that runs the full routing pipeline—including middleware, loaders, and actions—for a given `Request` object.

### Step 2: Execute Data Loading with query()

When a server request arrives, the handler's `query` method processes it. Implemented in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts) (lines 3799-3885), this method returns a `StaticHandlerContext` containing:

- **location**: The matched URL
- **loaderData**: Results from route loaders
- **actionData**: Results from actions
- **errors**: Any thrown errors captured by error boundaries
- **statusCode** and **headers**: HTTP metadata

The `query` function never throws; redirects are returned as raw `Response` objects that the server can forward directly to the client.

### Step 3: Build the Static Router

With the context prepared, `createStaticRouter` (located in [`packages/react-router/lib/dom/server.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/server.tsx), lines 78-95) constructs a stateless `DataRouter`. This router is **read-only**—its `state` property is populated from the `StaticHandlerContext`, but it cannot perform client-side navigation.

Attempts to call `push()`, `replace()`, or other navigation methods on the static router throw with a clear error message indicating that these operations are only available in browser environments.

### Step 4: Render with StaticRouterProvider

Finally, the `StaticRouterProvider` component (defined in [`packages/react-router/lib/dom/server.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/server.tsx), lines 57-84) renders the route tree using the static router and context. During rendering, it can emit a hydration script that injects loader data, action data, and errors into `window.__staticRouterHydrationData`.

This allows the client-side router to pick up exactly where the server left off without re-executing loaders.

## Complete React Router SSR Implementation

### Server-Side Entry (Express)

The following example demonstrates a complete server setup using Express. The `createStaticHandler` is instantiated once at startup, and each request is processed through the handler's `query` method.

```tsx
import { createStaticHandler, createStaticRouter, StaticRouterProvider } from "@react-router/server";
import { renderToString } from "react-dom/server";
import { routes } from "./routes";
import express from "express";

const app = express();

// Initialize the static handler once
const handler = createStaticHandler(routes);

app.all("*", async (req, res) => {
  // Execute loaders and actions for this request
  const result = await handler.query(req);
  
  if (result instanceof Response) {
    // Handle redirects or error responses from loaders/actions
    return res.status(result.status).send(await result.text());
  }

  // Create a read-only router populated with the loaded data
  const router = createStaticRouter(routes, result);

  // Render the application to HTML
  const html = renderToString(
    <StaticRouterProvider router={router} context={result} />
  );

  res.setHeader("Content-Type", "text/html");
  res.send(`<!doctype html>${html}`);
});

app.listen(3000);

```

Key implementation details:
- `handler.query(req)` performs all data fetching before rendering begins.
- If a loader or action returns a `Response` (such as a redirect), it is forwarded directly to the client.
- `createStaticRouter` receives the same routes array and the `StaticHandlerContext` to build a stateless router instance.

### Client-Side Hydration

On the client, the application hydrates using `createBrowserRouter`, which automatically reads the hydration data injected by the server.

```tsx
import { createBrowserRouter, RouterProvider } from "@react-router/react";
import { routes } from "./routes";

const router = createBrowserRouter(routes, {
  // The client reads window.__staticRouterHydrationData automatically
  // to restore loaderData, actionData, and errors without re-fetching
});

ReactDOM.hydrateRoot(
  document,
  <RouterProvider router={router} />
);

```

The `StaticRouterProvider` automatically emits a script tag that populates `window.__staticRouterHydrationData`, allowing the client router to resume application state seamlessly.

## Advanced SSR Patterns: Middleware Support

React Router's SSR implementation supports middleware through the `unstable_instrumentations` option. This allows you to wrap route handlers and modify responses during server-side execution.

```tsx
const handler = createStaticHandler(routes, {
  unstable_instrumentations: [{ route: myMiddlewareInstrumentation }],
});

const result = await handler.query(request, {
  generateMiddlewareResponse: (render) => {
    // Wrap the rendered context in a custom Response
    return new Response(render(), { 
      headers: { "x-middleware": "ok" } 
    });
  },
});

```

The `generateMiddlewareResponse` option hooks into the middleware pipeline and allows returning a raw `Response` that the server can send directly to the client.

## Key Source Files for React Router SSR

Understanding the implementation details requires examining these specific files in the `remix-run/react-router` repository:

- **[`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts)** — Implements `createStaticHandler` (lines 3733-3760) and the `query` method (lines 3799-3885) that executes loaders and actions on the server.

- **[`packages/react-router/lib/dom/server.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/server.tsx)** — Exports `createStaticRouter` (lines 78-95) for building stateless routers and `<StaticRouterProvider>` (lines 57-84) for server rendering.

- **[`packages/react-router/lib/rsc/server.rsc.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/server.rsc.ts)** — Server-side rendering entry for React Server Components, building on the same static handler logic for RSC environments.

- **[`packages/react-router/lib/server-runtime/server.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/server-runtime/server.ts)** — Helper utilities for creating static handler data routes.

- **[`packages/react-router/__tests__/router/ssr-test.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/__tests__/router/ssr-test.ts)** — Test suite verifying the full SSR lifecycle from handler initialization to hydration.

These files together define the SSR contract: a data-only `StaticHandler` for request processing, a stateless router for rendering, and a provider component that wires everything together while emitting hydration data.

## Summary

React Router's Server Side Rendering implementation separates data fetching from component rendering through a four-phase architecture:

- **Static Handler**: `createStaticHandler` processes route definitions and provides a `query` method to execute loaders and actions without browser APIs.
- **Data Execution**: The `query` method returns a `StaticHandlerContext` containing loader data, errors, and HTTP metadata, handling redirects as raw Response objects.
- **Stateless Router**: `createStaticRouter` builds a read-only router instance populated with server-fetched data that cannot perform client-side navigation.
- **Hydration**: `<StaticRouterProvider>` renders the route tree and injects `window.__staticRouterHydrationData`, allowing the client to resume without re-fetching.

This design ensures that React Router applications can render identical HTML on the server and hydrate seamlessly on the client using the same route definitions and data APIs.

## Frequently Asked Questions

### How does React Router handle redirects during SSR?

When a loader or action returns a redirect Response during server-side execution, the `handler.query()` method captures it and returns the raw Response object directly instead of a `StaticHandlerContext`. Your server code should check if the result is a Response instance and forward the status code, headers, and body to the client immediately, preventing unnecessary rendering.

### What is the difference between createStaticHandler and createStaticRouter?

`createStaticHandler` is a data-only engine defined in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts) that processes route definitions and executes loaders and actions against a Request object, returning a context object containing the results. `createStaticRouter` consumes that context to create a stateless, read-only React Router instance suitable for rendering components to HTML, but it cannot perform navigation operations like `push()` or `replace()`.

### How does hydration work with React Router SSR?

During server rendering, `<StaticRouterProvider>` automatically injects a script tag that populates `window.__staticRouterHydrationData` with the loader data, action data, errors, and router state. When the client initializes using `createBrowserRouter`, it reads this global variable to restore the application state without re-executing loaders, ensuring the client markup matches the server HTML exactly.

### Can I use middleware with React Router's SSR implementation?

Yes, React Router supports middleware through the `unstable_instrumentations` option in `createStaticHandler` and the `generateMiddlewareResponse` parameter in `handler.query()`. This allows you to wrap route handlers, modify responses, and implement cross-cutting concerns like authentication or logging while maintaining the static data flow required for server-side rendering.