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

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 (lines 3733-3760). This function transforms route definitions into a data-only structure capable of executing loaders and actions without browser APIs.

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 (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, 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, 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.

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.

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.

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:

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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →