Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

React Router Administrator & Developer Guide

Generated May 2026 from remix-run/react-router at b80380d

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide covers the complete architecture, configuration surface, deployment topology, security posture, and internal module reference for React Router v7 — a multi-strategy router for React supporting SPA, SSR, and static prerendering modes. It targets administrators deploying React Router applications in production, developers extending the framework or building custom adapters, and architecture reviewers evaluating the project.

For end-user installation, routing patterns, and everyday usage, see the User Guide.


1. Architecture Overview

1.1 High-Level System Design

React Router is a monorepo containing 11 packages that together form a full-stack React framework. The architecture separates concerns into four distinct layers:

Layer Packages Responsibility
Core Runtime react-router Route matching, data loading, navigation state, history management, React context distribution
Build Tooling @react-router/dev Vite plugin, CLI, config resolution, code generation, type generation, HMR
Server Adapters @react-router/express, @react-router/node, @react-router/cloudflare, @react-router/architect, @react-router/serve Platform-specific request/response translation, session storage
Routing Conventions @react-router/fs-routes, @react-router/remix-routes-option-adapter File-system routing, migration helpers

Additional packages: react-router-dom (legacy re-export shim), create-react-router (project scaffolding CLI).

1.2 Request Lifecycle

A request through a React Router SSR application follows this path:

HTTP Request
  → Server Adapter (Express/Node/Cloudflare/Architect)
    → createRequestHandler({ build, getLoadContext })
      → matchRoutes(url)
        → Execute loaders (parallel by default)
          → Render React tree with RouterProvider
            → Serialize to HTML stream
              → Server Adapter writes HTTP Response

For client-side navigations after hydration:

Link click / navigate()
  → history.pushState
    → matchRoutes(url)
      → Fetch loaders via /__data endpoint (or single-fetch turbo-stream)
        → Re-render changed route segments
          → ScrollRestoration

1.3 Rendering Strategies

React Router supports three rendering strategies configured via react-router.config.ts:

Strategy Config Build Output Runtime
SSR (default) ssr: true Client bundle + Server bundle Server renders on every request
SPA ssr: false Client bundle + static HTML shell Client-only rendering, no server
Prerender prerender: true | string[] | fn Client bundle + static HTML per path Static files served by any HTTP server

Note

When ssr: false, React Router generates a static HTML shell with the client bundle. Loaders still run but execute entirely on the client. Server-side loaders, actions, and headers exports are not available in SPA mode.

1.4 Module System

All packages produce dual ESM/CJS builds via tsup. The build pipeline uses __DEV__ defines to strip development-only code (warnings, validation) from production bundles. Each package's package.json uses conditional exports:

{
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    }
  }
}

2. System Requirements

2.1 Runtime Requirements

Requirement Version Notes
Node.js >= 20.0.0 Required for development and production server
React >= 18.0.0 React 19 supported with RSC features
TypeScript >= 5.1 Optional but strongly recommended; required for typegen

2.2 Build Requirements

Tool Version Notes
pnpm 9.10.0 Workspace protocol, catalog feature used
Vite 5.x, 6.x, 7-beta, 8.x Plugin supports multiple major versions
tsup Latest Package builds only (not application builds)

2.3 Platform Support

Platform Adapter Package Session Storage
Node.js (Express) @react-router/express Cookie, Memory, File
Node.js (raw HTTP) @react-router/node Cookie, Memory, File
Node.js (built-in) @react-router/serve Cookie, Memory, File
Cloudflare Workers/Pages @react-router/cloudflare Cookie, Memory, KV
AWS Lambda (Architect) @react-router/architect Cookie, Memory, DynamoDB

Important

The @react-router/serve package is designed for production use. It includes Express with compression, morgan logging, and static file serving with appropriate cache headers. It is the default server when deploying with react-router-serve.

2.4 Environment Variables

Variable Default Used By Purpose
NODE_ENV "development" All packages Controls __DEV__ flag, logging verbosity, error detail
PORT 3000 @react-router/serve, framework servers HTTP listen port
HOST "0.0.0.0" @react-router/serve HTTP bind address
REACT_ROUTER_ROOT process.cwd() @react-router/dev config loader Override project root directory
ARC_SANDBOX None @react-router/architect Architect sandbox path for local development

3. Configuration Reference

3.1 react-router.config.ts

The primary configuration file lives at the project root. It must provide a default export of type Config:

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

export default {
  // All options shown with defaults
} satisfies Config;

Complete Option Reference

Option Type Default Description
appDirectory string "app" Directory containing application source (routes, entry files, root.tsx)
basename string "/" Base path for all routes. Set when deploying to a subdirectory
buildDirectory string "build" Output directory for production builds
serverBuildFile string "index.js" Filename for the server bundle entry point
serverModuleFormat "esm" | "cjs" "esm" Module format for server bundle output
ssr boolean true Enable server-side rendering. false produces SPA mode
prerender boolean | string[] | Function | Object false Static prerendering configuration (see below)
routeDiscovery Object { mode: "lazy" } Controls how the route manifest is delivered to the client
serverBundles Function None Split server code into named bundles based on route branches
allowedActionOrigins string[] [] Additional origins allowed for form submissions (CSRF)
future Object {} Feature flags for upcoming versions
presets Preset[] [] Configuration presets that can override settings
buildEnd Function None Hook called after build completes

Prerender Configuration

The prerender option accepts several forms:

// Prerender all static routes
prerender: true

// Prerender specific paths
prerender: ["/", "/about", "/pricing"]

// Dynamic path resolution
prerender: async ({ getStaticPaths }) => {
  const paths = await getStaticPaths();
  return ["/", ...paths];
}

// Object form with concurrency control
prerender: {
  paths: ["/", "/about"],
  unstable_concurrency: 4, // Parallel prerender workers
}

Route Discovery

routeDiscovery: {
  mode: "lazy",          // "lazy" (default) or "initial"
  manifestPath: undefined // Custom path for the route manifest
}

In lazy mode, the route manifest is fetched on demand as the user navigates. In initial mode, the full manifest is included in the initial HTML payload — larger initial load but no manifest fetch waterfall.

Server Bundles

serverBundles: ({ branch }) => {
  // branch is the array of matched routes
  const isAdmin = branch.some(r => r.id.startsWith("routes/admin"));
  return isAdmin ? "admin" : "main";
}

This splits the server build into separate bundles, useful for deploying different route trees to different infrastructure (e.g., admin routes to a separate Lambda).

Future Flags

future: {
  v8_middleware: boolean,                          // Enable route middleware
  v8_splitRouteModules: boolean | "enforce",       // Tree-shake unused route exports
  v8_viteEnvironmentApi: boolean,                  // Use Vite's Environment API
  unstable_optimizeDeps: boolean,                  // Pre-bundle dependencies
  unstable_passThroughRequests: boolean,            // Pass unmatched requests through
  unstable_subResourceIntegrity: boolean,           // SRI hashes on script/link tags
  unstable_trailingSlashAwareDataRequests: boolean, // Match trailing slashes in data requests
  unstable_previewServerPrerendering: boolean,      // Preview prerendered output in dev
}

Warning

Flags prefixed with v8_ will become the default behavior in React Router v8 and cannot be disabled after upgrade. Plan migration early. Flags prefixed with unstable_ may change or be removed without notice.

3.2 Route Configuration via routes.ts

Routes are defined in app/routes.ts using helper functions:

import {
  route,
  index,
  layout,
  prefix,
  relative,
  getAppDirectory,
} from "@react-router/dev/routes";
import type { RouteConfig } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("about", "routes/about.tsx"),

  layout("routes/auth/layout.tsx", [
    route("login", "routes/auth/login.tsx"),
    route("register", "routes/auth/register.tsx"),
  ]),

  ...prefix("api", [
    route("users", "routes/api/users.tsx"),
    route("users/:id", "routes/api/users.$id.tsx"),
  ]),
] satisfies RouteConfig;

Route Helper API

Helper Signature Purpose
route route(path, file, children?) Define a route with a URL path
index index(file) Define an index route (renders at parent path)
layout layout(file, children?) Define a pathless layout wrapper
prefix prefix(path, children) Add a path prefix to child routes without a layout
relative relative(dir, routes) Resolve file paths relative to a directory
getAppDirectory getAppDirectory() Returns the configured appDirectory path

3.3 File-System Routing (@react-router/fs-routes)

When using the @react-router/fs-routes package, routes are derived from the filesystem:

// app/routes.ts
import { flatRoutes } from "@react-router/fs-routes";

export default flatRoutes();

Naming Conventions

Pattern URL Example File
users.tsx /users Layout for /users/*
users._index.tsx /users (index) Index route
users.$id.tsx /users/:id Dynamic parameter
users.($id).tsx /users/:id? Optional parameter
files.$.tsx /files/* Splat/catch-all
_auth.login.tsx /login Pathless layout child
[sitemap.xml].tsx /sitemap.xml Escaped literal dots
users_.new.tsx /users/new Opt out of parent layout

Supported file extensions: .js, .jsx, .ts, .tsx, .md, .mdx.

Tip

The . character in filenames translates to / in URL paths. Use [brackets] to include literal characters that would otherwise be interpreted as routing syntax. For example, [sitemap.xml].tsx creates a route at /sitemap.xml.

3.4 Vite Configuration

The React Router Vite plugin wraps around Vite's standard configuration:

// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouter()],
});

Cloudflare Configuration

import { reactRouter } from "@react-router/dev/vite";
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    cloudflareDevProxy(),
    reactRouter(),
  ],
});

Or with the official Cloudflare plugin:

import { reactRouter } from "@react-router/dev/vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: "ssr" } }),
    reactRouter(),
  ],
});

3.5 Entry Files

Two optional entry files control the client and server rendering boundaries:

app/entry.client.tsx — Controls client-side hydration:

import { HydratedRouter } from "react-router/dom";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <HydratedRouter />
    </StrictMode>
  );
});

app/entry.server.tsx — Controls server-side rendering:

import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";
import { ServerRouter } from "react-router";
import type { EntryContext } from "react-router";

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  routerContext: EntryContext,
) {
  const stream = await renderToReadableStream(
    <ServerRouter context={routerContext} url={request.url} />,
    {
      signal: request.signal,
      onError(error) {
        responseStatusCode = 500;
        console.error(error);
      },
    }
  );

  if (isbot(request.headers.get("user-agent"))) {
    await stream.allReady;
  }

  return new Response(stream, {
    status: responseStatusCode,
    headers: responseHeaders,
  });
}

Use npx react-router reveal entry.client or npx react-router reveal entry.server to generate default entry files for customization.


4. Deployment & Scaling

4.1 Build Output

Running react-router build produces:

build/
  client/
    assets/           # Hashed JS/CSS chunks (immutable, cache 1 year)
    favicon.ico       # Static assets from public/
  server/
    index.js          # Server entry point (or custom serverBuildFile)

Important

Files in build/client/assets/ use content-hash filenames and should be served with Cache-Control: public, max-age=31536000, immutable. The @react-router/serve package handles this automatically. Custom server setups must configure this explicitly.

4.2 Deployment Targets

The simplest production deployment:

npx react-router-serve build/server/index.js

This starts an Express server with:

  • Compression middleware (gzip/brotli)
  • Morgan access logging
  • Static file serving with appropriate cache headers
  • Source map support (when NODE_ENV !== "production")

Environment variables: PORT (default 3000), HOST (default 0.0.0.0).

For RSC builds, react-router-serve detects the build type and uses the appropriate rendering pipeline.

Node.js with Express

import express from "express";
import { createRequestHandler } from "@react-router/express";

const app = express();

// Serve static assets with long-term caching
app.use(
  "/assets",
  express.static("build/client/assets", {
    immutable: true,
    maxAge: "1y",
  })
);

// Serve other static files
app.use(express.static("build/client", { maxAge: "1h" }));

// Handle SSR requests
app.all(
  "*",
  createRequestHandler({
    build: await import("./build/server/index.js"),
    getLoadContext: (req, res) => ({
      // Custom context available in loaders/actions
    }),
  })
);

app.listen(3000);

Node.js Raw HTTP Server

import { createServer } from "node:http";
import { createRequestListener } from "@react-router/node";

const listener = createRequestListener({
  build: await import("./build/server/index.js"),
});

createServer(listener).listen(3000);

Cloudflare Workers

// server.ts
import { createRequestHandler } from "@react-router/cloudflare";

const handler = createRequestHandler(
  // @ts-expect-error virtual module
  () => import("virtual:react-router/server-build"),
  import.meta.env.MODE
);

export default {
  fetch(request, env, ctx) {
    return handler(request, {
      cloudflare: { env, ctx },
    });
  },
};

AWS Lambda via Architect

// server.ts
import { createRequestHandler } from "@react-router/architect";

export const handler = createRequestHandler({
  build: await import("./build/server/index.js"),
});

4.3 Server Bundles for Multi-Region / Split Deploys

The serverBundles config option splits the server build by route branch, enabling deployment of different route trees to different infrastructure:

// react-router.config.ts
export default {
  serverBundles: ({ branch }) => {
    if (branch.some(r => r.id.startsWith("routes/api"))) {
      return "api";     // Deploy to edge workers
    }
    return "ssr";       // Deploy to origin server
  },
} satisfies Config;

This produces separate bundles in build/server/api/index.js and build/server/ssr/index.js.

4.4 Static Prerendering

For sites that can be fully or partially prerendered:

export default {
  prerender: {
    paths: async ({ getStaticPaths }) => {
      const posts = await fetchBlogPosts();
      return [
        "/",
        "/about",
        ...posts.map(p => `/blog/${p.slug}`),
      ];
    },
    unstable_concurrency: 4,
  },
} satisfies Config;

Prerendered output goes to build/client/ as static HTML files. These can be served by any static file server (nginx, CDN, S3+CloudFront).

4.5 Docker Deployment

FROM node:20-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine AS production
WORKDIR /app
COPY --from=build /app/build ./build
COPY --from=build /app/package.json ./
COPY --from=build /app/node_modules ./node_modules
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npx", "react-router-serve", "build/server/index.js"]

4.6 Scaling Considerations

React Router SSR applications are stateless by default (session state lives in cookies or external stores). Horizontal scaling requires:

  1. Session storage — Use cookie-based sessions (no server affinity needed) or external stores (Redis, DynamoDB, Cloudflare KV)
  2. Static assets — Serve from CDN. The content-hashed filenames in build/client/assets/ enable aggressive caching
  3. Server bundles — Use serverBundles to split compute-heavy routes to dedicated infrastructure
  4. Prerendering — Prerender static pages to eliminate server compute entirely

5. Operations

5.1 CLI Reference

The react-router CLI provides five commands:

# Production build
react-router build [projectDir]
# Outputs: build/client/ and build/server/

# Development server with HMR
react-router dev [projectDir]
# Starts Vite dev server with SSR middleware

# Display route tree
react-router routes [projectDir]
# Add --json for JSON output

# Generate entry files for customization
react-router reveal [entry.client|entry.server]
# Creates app/entry.client.tsx or app/entry.server.tsx

# Generate route types
react-router typegen
# Add --watch for watch mode
# Outputs to .react-router/types/

5.2 Type Generation

React Router automatically generates TypeScript types for route modules. The typegen system produces types in .react-router/types/ that provide type safety for:

  • Loader and action function parameters
  • useLoaderData() return types
  • Route parameters (useParams())
  • Search parameters

Generated types are placed alongside route files in the type output directory:

.react-router/
  types/
    app/
      routes/
        +types.home.ts
        +types.users.$id.ts

Each generated file exports types specific to that route's parameters, loader data, and action data. These types are automatically imported when using the Route.LoaderArgs, Route.ActionArgs, and similar type helpers.

Note

Run react-router typegen --watch during development to keep types in sync as routes change. The Vite plugin runs typegen automatically during react-router dev.

5.3 Build Output Verification

After building, verify the output:

# Check build output exists
ls -la build/client/assets/
ls -la build/server/

# Verify server bundle is valid
node -e "import('./build/server/index.js').then(m => console.log('OK', Object.keys(m)))"

# Check asset sizes
du -sh build/client/assets/*

5.4 Health Monitoring

For production SSR deployments, implement health checks:

// app/routes/healthz.tsx
export function loader() {
  return new Response("OK", {
    status: 200,
    headers: { "Cache-Control": "no-store" },
  });
}

Key metrics to monitor:

Metric Source Alert Threshold
SSR response time Server logs / APM p99 > 500ms
Loader execution time Custom instrumentation Individual loader > 200ms
Client bundle size Build output Main chunk > 250KB gzipped
Hydration time Client-side RUM > 2s on 3G
Error boundary hits Server logs Any increase

5.5 Logging

@react-router/serve uses Morgan for HTTP access logging. For custom servers, instrument at the adapter level:

app.all("*", (req, res, next) => {
  const start = Date.now();
  res.on("finish", () => {
    console.log(JSON.stringify({
      method: req.method,
      path: req.path,
      status: res.statusCode,
      duration: Date.now() - start,
    }));
  });
  next();
}, createRequestHandler({ build }));

5.6 Error Handling

React Router uses a hierarchical error boundary system:

  1. Route-level ErrorBoundary — Catches errors in that route's loader, action, or component
  2. Parent route ErrorBoundary — Catches errors that bubble up from child routes
  3. Root ErrorBoundary — Last resort, defined in app/root.tsx
  4. Server-level error handler — entry.server.tsx onError callback for streaming errors
// app/root.tsx
export function ErrorBoundary() {
  const error = useRouteError();
  if (isRouteErrorResponse(error)) {
    return <div>{error.status} {error.statusText}</div>;
  }
  return <div>Unexpected error</div>;
}

Caution

Never expose raw error messages or stack traces to end users in production. The ErrorBoundary component receives the full error object, but your rendering logic should display a user-friendly message. Set NODE_ENV=production to suppress detailed error output from the framework.


6. Performance

6.1 Data Loading Optimization

React Router executes loaders in parallel for all matched routes. This is the primary performance lever:

GET /users/123/posts
  → root loader      ─┐
  → users loader      ├── All execute in parallel
  → users.$id loader  │
  → posts loader     ─┘

Optimize loaders by:

  1. Avoiding waterfall dependencies — Each loader should fetch its own data independently
  2. Using shouldRevalidate — Prevent unnecessary re-fetching on navigations that don't affect a route's data
  3. Deferring non-critical data — Return promises from loaders to stream data after the initial shell
export function shouldRevalidate({
  currentUrl,
  nextUrl,
  defaultShouldRevalidate,
}) {
  // Only revalidate if the search params changed
  if (currentUrl.searchParams.toString() === nextUrl.searchParams.toString()) {
    return false;
  }
  return defaultShouldRevalidate;
}

6.2 Code Splitting

React Router automatically code-splits by route. Each route module becomes a separate chunk loaded on demand. Additional strategies:

  • v8_splitRouteModules — When enabled, tree-shakes unused exports from route modules. If a route only exports a loader and default component (no action, no ErrorBoundary), the unused exports are eliminated
  • routeDiscovery.mode: "lazy" — The route manifest itself is loaded incrementally, reducing initial payload for large route trees
  • Dynamic imports within components — Use React.lazy() for heavy sub-components within a route

6.3 Caching Strategy

Asset Type Path Pattern Cache Header
Hashed assets /assets/* public, max-age=31536000, immutable
Static files /favicon.ico, etc. public, max-age=3600
HTML pages /* no-cache or short TTL
Data requests /__data Set via loader headers export

Each route can export a headers function to control response caching:

export function headers({ loaderHeaders }) {
  return {
    "Cache-Control": loaderHeaders.get("Cache-Control") ?? "public, max-age=300",
  };
}

6.4 Bundle Analysis

Vite's built-in build.rollupOptions can be used with analysis plugins:

// vite.config.ts
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  plugins: [reactRouter()],
  build: {
    rollupOptions: {
      plugins: [visualizer({ open: true })],
    },
  },
});

6.5 Streaming SSR

React Router streams HTML by default using renderToReadableStream. The initial shell renders immediately with <Suspense> boundaries, and deferred data streams in as it resolves. This provides fast Time to First Byte (TTFB) while still server-rendering the complete page.

For bot detection (ensuring crawlers receive complete HTML):

// entry.server.tsx
if (isbot(request.headers.get("user-agent"))) {
  await stream.allReady; // Wait for all content before responding
}

7. Security

7.1 CSRF Protection

React Router includes built-in CSRF protection for form submissions (actions). The mechanism in packages/react-router/lib/actions.ts:

  1. On every non-GET request, throwIfPotentialCSRFAttack() compares the Origin header against Host and X-Forwarded-Host headers
  2. If the origin does not match, the request is rejected with a 403 response
  3. Additional origins can be allowed via allowedActionOrigins in the config
// react-router.config.ts
export default {
  allowedActionOrigins: [
    "https://admin.example.com",
    "*.example.com",        // Wildcard subdomain
    "**.example.com",       // Deep wildcard
  ],
} satisfies Config;

Wildcard matching uses micromatch glob patterns.

Warning

The CSRF check relies on the Origin header sent by browsers. API clients that do not send an Origin header (e.g., curl, server-to-server calls) will be blocked. For API routes that accept non-browser requests, handle CSRF differently (e.g., API tokens).

7.2 Session Security

import { createCookieSessionStorage } from "react-router";

const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    cookie: {
      name: "__session",
      httpOnly: true,      // Not accessible via JavaScript
      maxAge: 60 * 60 * 24, // 24 hours
      path: "/",
      sameSite: "lax",     // CSRF protection
      secrets: ["s3cr3t"], // Signing key(s) — first is active, rest for rotation
      secure: true,        // HTTPS only
    },
  });

Session security checklist:

Setting Required Value Risk if Wrong
httpOnly true XSS can steal session cookies
secure true (production) Session sent over HTTP, interceptable
sameSite "lax" or "strict" CSRF via cross-site form submissions
secrets Non-empty array Unsigned cookies, trivially forgeable
maxAge Bounded value Sessions that never expire

Caution

Cookie session secrets should be cryptographically random strings of at least 32 characters. Rotate secrets by prepending new secrets to the array — the first secret signs new cookies, while older secrets verify existing cookies during the rotation period.

Server-Side Session Storage

For sessions that should not be stored in cookies (large payloads, sensitive data):

Storage Package Use Case
createFileSessionStorage() @react-router/node Single-server deployments
Cloudflare KV @react-router/cloudflare Cloudflare Workers
DynamoDB @react-router/architect AWS deployments
createMemorySessionStorage() react-router Development/testing only

7.3 Content Security

React Router does not inject inline scripts by default in SSR mode. The hydration data is serialized as a <script> tag with a deterministic structure. For strict CSP:

// entry.server.tsx — add nonce to scripts
export default function handleRequest(
  request: Request,
  status: number,
  headers: Headers,
  context: EntryContext,
) {
  const nonce = crypto.randomUUID();
  headers.set(
    "Content-Security-Policy",
    `script-src 'nonce-${nonce}' 'strict-dynamic';`
  );
  // Pass nonce to renderToReadableStream options
}

The unstable_subResourceIntegrity future flag adds SRI hashes to <script> and <link> tags for additional integrity verification.

7.4 Module Boundary Enforcement

React Router enforces server/client module boundaries:

  • Files/directories named .server (e.g., db.server.ts, .server/) are excluded from client bundles
  • Files/directories named .client are excluded from server bundles
  • Attempting to import a .server module from client code produces a build error

Tip

Use the .server suffix convention for any module containing secrets, database connections, or server-only dependencies. This prevents accidental bundling of sensitive code into the client JavaScript.

7.5 Headers Export

Each route module can export a headers function to set security headers:

export function headers() {
  return {
    "X-Frame-Options": "DENY",
    "X-Content-Type-Options": "nosniff",
    "Referrer-Policy": "strict-origin-when-cross-origin",
    "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
  };
}

For headers that apply to all routes, set them at the server adapter level (Express middleware, Cloudflare Workers headers, etc.).


8. Developer Internals

8.1 Core Router Engine

The router core lives in packages/react-router/lib/router/ and implements:

History Management — Wraps the browser History API (or in-memory history for SSR/tests) with:

  • pushState / replaceState abstraction
  • Blocker support (preventing navigation with unsaved changes)
  • Pop state event handling

Route Matching — Implements a ranked route matching algorithm:

  1. Routes are scored by specificity (static segments > dynamic > splat)
  2. Matching considers nested route trees with relative paths
  3. PrefixLookupTrie (in @react-router/fs-routes) provides O(prefix-length) lookup for file-system routes

Data Loading — The data pipeline:

  1. Match routes for the target URL
  2. Determine which loaders need to run (respecting shouldRevalidate)
  3. Execute loaders in parallel via Promise.all
  4. For actions: execute the action first, then revalidate affected loaders
  5. Handle redirects at any point in the pipeline
  6. Commit navigation state and trigger React re-render

8.2 React Context Architecture

React Router distributes state through several React contexts defined in packages/react-router/lib/context.tsx:

Context Purpose Consumer Hooks
DataRouterContext Router instance, navigator useNavigate, useSubmit
DataRouterStateContext Current router state (location, matches, errors) useLocation, useMatches
RouteContext Current route match, outlet useOutlet, useParams
RouteErrorContext Error for current route useRouteError
NavigationContext Navigation helpers useNavigate, useLinkClickHandler

8.3 Component Hierarchy

<RouterProvider router={router}>
  <DataRouterContext.Provider>
    <DataRouterStateContext.Provider>
      <Router>
        <NavigationContext.Provider>
          <LocationContext.Provider>
            <Routes>
              <Route element={<RootLayout />}>
                <Outlet />  ← Renders matched child route
              </Route>
            </Routes>
          </LocationContext.Provider>
        </NavigationContext.Provider>
      </Router>
    </DataRouterStateContext.Provider>
  </DataRouterContext.Provider>
</RouterProvider>

8.4 Key Internal Classes

Class Location Purpose
PrefixLookupTrie packages/react-router-fs-routes Trie-based route matching for file-system routes
StreamPump packages/react-router-node Converts Web ReadableStream to Node.js streams
Deferred packages/react-router/lib/components.tsx Internal promise wrapper for <Await> component
AwaitErrorBoundary packages/react-router/lib/components.tsx Error boundary wrapping deferred data resolution
RenderErrorBoundary packages/react-router/lib/components.tsx Error boundary for route component rendering
PlaywrightFixture Integration tests Helper for Playwright-based integration tests
CopyTemplateError packages/create-react-router Error during project scaffolding

8.5 Vite Plugin Internals

The Vite plugin (packages/react-router-dev/vite/) orchestrates the build and dev experience:

Config Resolution:

  1. Load react-router.config.ts via Vite's module resolution
  2. Apply presets (presets can modify any config option)
  3. Resolve route tree from routes.ts or file-system convention
  4. Generate virtual modules for server build entry

Development Server:

  1. Vite dev server starts with SSR middleware
  2. Routes are watched for changes → automatic typegen
  3. HMR handles component updates without full page reload
  4. Server modules are re-evaluated on change (no server restart needed)

Production Build:

  1. Client build: Vite builds the client entry with route-based code splitting
  2. Server build: Vite builds the server entry with route handlers inlined
  3. Manifest generation: Asset manifest for SSR → client asset resolution
  4. Type generation: Final pass of route types

Virtual Modules:

  • virtual:react-router/server-build — Resolved server build for adapters
  • virtual:react-router/browser-manifest — Client route manifest

8.6 RSC Support (Unstable)

React Server Components support lives in packages/react-router/lib/rsc/ and is considered unstable. It provides:

  • Server-side component rendering without client JavaScript
  • Integration with React's streaming SSR pipeline
  • @react-router/serve detects RSC builds and uses the appropriate server entry

8.7 Module Analysis by Package

react-router (Core)

Module Path Purpose
lib/router/router.ts Core router state machine, navigation, data loading
lib/router/history.ts History abstraction (browser, hash, memory)
lib/router/utils.ts Route matching, path parsing, ranking
lib/components.tsx RouterProvider, Route, Routes, Outlet, Navigate, Await
lib/hooks.tsx useNavigate, useLocation, useParams, useLoaderData, useActionData, useRouteError, etc.
lib/dom/global.ts BrowserRouter, HashRouter setup
lib/dom/lib.tsx Link, NavLink, Form, ScrollRestoration, useFetcher, useSearchParams
lib/dom/ssr/components.tsx HydratedRouter, ServerRouter, Meta, Links, Scripts
lib/dom/ssr/entry.ts EntryContext for SSR
lib/dom/ssr/routes.tsx Route module loading and caching
lib/dom/ssr/single-fetch.tsx Single-fetch data endpoint (turbo-stream)
lib/server-runtime/server.ts createRequestHandler for server adapters
lib/server-runtime/sessions.ts Session API, createSessionStorage
lib/server-runtime/cookies.ts Cookie parsing, signing, serialization
lib/server-runtime/data.ts Loader/action data serialization
lib/context.tsx All React context definitions
lib/actions.ts CSRF protection, action handling
lib/rsc/ React Server Components (unstable)

@react-router/dev (Build Tooling)

Module Path Purpose
vite/plugin.ts Main Vite plugin entry point
vite/dev.ts Development server middleware
vite/build.ts Production build orchestration
vite/styles.ts CSS handling and HMR
config/config.ts Configuration loading and validation
config/routes.ts Route tree resolution
typegen/index.ts Type generation orchestrator
typegen/generate.ts Route type file generation
cli/run.ts CLI command dispatch

@react-router/serve (Production Server)

Module Path Purpose
src/index.ts Express server setup: compression, morgan, static serving, request handler
bin/react-router-serve.ts CLI entry point, port/host resolution

@react-router/node

Module Path Purpose
sessions/fileStorage.ts File-based session storage with atomic writes
stream.ts Web stream ↔ Node.js stream conversion via StreamPump
index.ts createRequestListener for raw HTTP servers

@react-router/cloudflare

Module Path Purpose
worker.ts Cloudflare Workers request handler adapter
sessions/kvStorage.ts Cloudflare KV session storage

@react-router/architect

Module Path Purpose
server.ts AWS Lambda event → Web Request conversion
sessions/dynamodbStorage.ts DynamoDB session storage

@react-router/fs-routes

Module Path Purpose
flatRoutes.ts File-system to route tree conversion
routeManifest.ts Route manifest generation from filesystem

8.8 Testing Infrastructure

Unit Tests (Jest):

  • Each package has jest.config.js extending shared config from jest/jest.config.shared
  • Tests co-located with source or in __tests__/ directories
  • createRoutesStub() utility for testing route modules in isolation

Integration Tests (Playwright):

  • Located in integration/ directory
  • playwright.config.ts at integration root
  • PlaywrightFixture class provides helpers for:
    • Starting dev/production servers
    • Navigation and interaction
    • Network request interception
    • Console log capture
// Testing a route module
import { createRoutesStub } from "react-router";
import { render, screen } from "@testing-library/react";

const Stub = createRoutesStub([
  {
    path: "/",
    Component: MyComponent,
    loader: () => ({ message: "hello" }),
  },
]);

render(<Stub initialEntries={["/"]} />);

9. Integration Points

9.1 create-react-router (Scaffolding)

The create-react-router package provides project scaffolding:

npx create-react-router@latest my-app
# Or with a specific template
npx create-react-router@latest my-app --template remix-run/react-router/templates/basic

Templates are fetched from GitHub repositories. The scaffolding process:

  1. Clone or download the template
  2. Replace template placeholders
  3. Initialize git repository
  4. Display setup instructions

CopyTemplateError is thrown when template files cannot be written (permissions, disk space).

9.2 MDX Integration

React Router supports MDX files as route modules when configured with an MDX Vite plugin:

// vite.config.ts
import mdx from "@mdx-js/rollup";
import { reactRouter } from "@react-router/dev/vite";

export default defineConfig({
  plugins: [mdx(), reactRouter()],
});

MDX files (.md, .mdx) in the routes directory are treated as route modules. They can export loader, action, headers, meta, and handle alongside their content.

9.3 Presets System

Presets allow third-party packages to provide React Router configuration:

// A preset package
import type { Preset } from "@react-router/dev/config";

export function myPreset(): Preset {
  return {
    name: "my-preset",
    reactRouterConfig: () => ({
      serverModuleFormat: "cjs",
      // Any config option can be set
    }),
    reactRouterVitePlugin: () => ({
      // Vite plugin options
    }),
  };
}

// Usage in react-router.config.ts
import { myPreset } from "my-preset";

export default {
  presets: [myPreset()],
} satisfies Config;

Presets are applied in order; later presets override earlier ones. Direct config options override all presets.

9.4 getLoadContext

Every server adapter supports getLoadContext to inject server-specific context into loaders and actions:

// Express
createRequestHandler({
  build,
  getLoadContext: (req, res) => ({
    db: req.app.locals.db,
    user: req.user,
  }),
});

// Cloudflare Workers
handler(request, {
  cloudflare: { env, ctx },
});

The context is available as the second argument to loaders and actions:

export function loader({ request, context }) {
  const db = context.db;
  // ...
}

9.5 @react-router/remix-routes-option-adapter

For projects migrating from Remix, this adapter translates Remix's routes option format to React Router's routes.ts format:

// app/routes.ts
import { remixRoutesOptionAdapter } from "@react-router/remix-routes-option-adapter";

export default remixRoutesOptionAdapter((defineRoutes) => {
  return defineRoutes((route) => {
    route("/", "routes/home.tsx", { index: true });
    route("/about", "routes/about.tsx");
  });
});

9.6 Known Issues and Workarounds

Issue Description Workaround
#15007 Concurrent Vite config resolution leaks route state when using flatRoutes() Avoid parallel Vite config resolution in custom tooling
#14754 Duplicated styles with suspended components since v7.9.5 Pin to v7.9.4 or monitor for patch
#14997 Server-only module error with .server directory naming Use .server.ts file suffix instead of .server/ directory
#14991 pnpm requires trustPolicyExclude for @react-router/dev Add @react-router/dev to pnpm's trustPolicyExclude
#14744 Typegen bug with * in folder names Avoid * in directory names
#14903 Middleware not handling redirects with useFetcher Handle redirects in the fetcher action directly

Note

Check the React Router GitHub repository for the latest status on these issues. Many may be resolved in newer releases beyond the source commit analyzed here.

9.7 Build End Hook

The buildEnd hook runs after the production build completes:

// react-router.config.ts
export default {
  buildEnd: async ({ buildManifest, reactRouterConfig, viteConfig }) => {
    // Generate sitemap from build manifest
    // Upload assets to CDN
    // Notify deployment pipeline
  },
} satisfies Config;

9.8 Middleware (v8 Future Flag)

When future.v8_middleware is enabled, route modules can export middleware:

export const unstable_middleware = [
  async function authMiddleware({ request, context }, next) {
    const session = await getSession(request);
    context.user = session.get("user");
    return next();
  },
];

Middleware runs before loaders/actions and can modify context, short-circuit with responses, or pass through to the next handler.

9.9 Quick Reference Tables

Exported Functions from Route Modules

Export Purpose Server/Client
default Route component Client (+ SSR)
loader Data loading (GET) Server (or client in SPA)
clientLoader Client-side data loading Client only
action Mutation handling (POST/PUT/DELETE) Server (or client in SPA)
clientAction Client-side mutation handling Client only
ErrorBoundary Error UI for this route Client (+ SSR)
headers HTTP response headers Server
handle Arbitrary data exposed via useMatches Both
links <link> tags for this route Both
meta <meta> and <title> tags Both
shouldRevalidate Control loader re-execution Client
HydrateFallback UI shown during client hydration Client
unstable_middleware Request middleware (v8 flag) Server

Core Hooks Reference

Hook Returns Purpose
useLoaderData() Loader return value Access route loader data
useActionData() Action return value Access route action data
useParams() Params URL parameters
useSearchParams() [URLSearchParams, setter] Query string parameters
useLocation() Location Current location object
useNavigate() NavigateFunction Programmatic navigation
useNavigation() Navigation Pending navigation state
useRouteError() unknown Error caught by error boundary
useMatches() UIMatch[] All matched routes with handle data
useFetcher() Fetcher Non-navigation data loading/mutations
useFetchers() Fetcher[] All active fetchers
useRouteLoaderData(routeId) Loader data Access another route's loader data
useRevalidator() { revalidate, state } Manually trigger revalidation
useBlocker() Blocker Prevent navigation (unsaved changes)