# React Router Data Router Architecture: A Deep Dive into the Core Engine

> Explore the React Router data router architecture. Discover how its three layers orchestrate loaders, actions, and SSR through a unified navigation pipeline for efficient data handling.

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

---

**React Router's data router architecture consists of three tightly-coupled layers—a stateful Router Instance, a normalized Route/Data Model, and a pluggable Data Strategy Engine—that centrally orchestrate loaders, actions, fetchers, revalidation, and SSR flows through a unified navigation pipeline.**

The `remix-run/react-router` data router architecture moves routing and data logic out of React components into a framework-agnostic core. This engine manages navigation state, history synchronization, and data mutations via a centralized state machine that powers both client-side navigation and server-side rendering.

## Core Architectural Layers

### Router Instance Layer

The **Router Instance** serves as the primary state container and public API surface. In [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts), the `createRouter` function initializes a state machine that tracks navigation status (`idle|loading|submitting`) through the `RouterState` interface (lines 24‑84).

The router exposes methods like `navigate`, `fetch`, `revalidate`, and `subscribe` (lines 66‑78). These are thin wrappers around an internal **navigation pipeline** (`startNavigation` → `completeNavigation`). History integration is injected at creation time via `init.history.listen` (lines ≈ 1120‑1150), allowing the router to respond to browser back/forward events using the same pipeline as programmatic navigation.

### Route and Data Model

The architecture uses **agnostic route objects** defined in [`packages/react-router/lib/router/utils.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts). The `AgnosticDataRouteObject` type (lines ≈ 370‑420) describes routes with `id`, `path`, optional `loader`, `action`, `middleware`, and `lazy` properties.

The `convertRoutesToDataRoutes` function (lines ≈ 800‑860) transforms user-supplied route trees into a flat **RouteManifest**, generating unique IDs and normalizing index routes. Matching is performed by `matchRoutes` (lines ≈ 902‑945) and `matchRouteBranch` (lines ≈ 1230‑1280), which return ordered arrays of `AgnosticDataRouteMatch` objects containing params, route references, and matched pathnames.

### Data Strategy Engine

The **Data Strategy Engine** is the architectural core that decides exactly which routes must execute their loaders or actions during navigation. This logic is isolated in a pluggable `DataStrategyFunction` (lines ≈ 1520‑1530 in [`utils.ts`](https://github.com/remix-run/react-router/blob/main/utils.ts)) so developers can override behavior for custom middleware or client-side caching.

Each matched route is wrapped in a `DataStrategyMatch` (lines ≈ 1250‑1295) exposing:
- `shouldLoad` – Boolean indicating if the route needs data
- `shouldRevalidateArgs` – Parameters for custom revalidation hooks
- `resolve()` – Lazy loader for `route.lazy` code that runs handlers only when needed

Results are aggregated as `DataStrategyResult` objects (`type: "data" | "error"`), then merged into global state via `mergeLoaderData` (lines ≈ 1470‑1495 in [`router.ts`](https://github.com/remix-run/react-router/blob/main/router.ts)), preserving existing data for routes that did not require reloading.

## Navigation Pipeline and Revalidation

Navigation flows through `startNavigation`, which prepares the next state, then `completeNavigation`, which commits results. The pipeline handles:
- **Loader execution** – Parallel data fetching for matched routes
- **Action handling** – Sequential mutation execution before loaders
- **Error boundaries** – Catching and bubbling route-level errors

The `revalidate` function (lines ≈ 1405‑1415) creates a *virtual navigation* (no URL change) that forces the data strategy to rerun. It respects per-route `shouldRevalidate` hooks and the `unstable_defaultShouldRevalidate` flag to optimize network requests.

## Server-Side Rendering Support

For SSR, the architecture provides `StaticHandler` (lines ≈ 1540‑1570 in [`router.ts`](https://github.com/remix-run/react-router/blob/main/router.ts)), accessed via `createStaticHandler` from `react-router/server`. This server-only variant runs the identical data strategy as the client, returning a `StaticHandlerContext` containing loader data, errors, and HTTP status codes for response generation.

## Fetchers as Sub-Routers

**Fetchers** act as lightweight, isolated data loaders that share the main router's pipeline. Defined in [`router.ts`](https://github.com/remix-run/react-router/blob/main/router.ts) (lines ≈ 640‑720), fetchers maintain their own `state`, `loaderData`, and `actionData` maps but utilize the same `DataStrategy` logic. The public API `router.fetch(key, routeId, href, opts)` (lines ≈ 186‑200) initiates fetcher requests without triggering URL navigation.

## Practical Implementation Examples

### Creating a Browser Data Router

The following demonstrates route IDs, loaders, actions, and nested routes handled by the data router architecture:

```tsx
import {
  createBrowserRouter,
  RouterProvider,
  redirect,
} from "react-router";

const router = createBrowserRouter([
  {
    path: "/",
    id: "root",
    loader: async () => {
      const res = await fetch("/api/user");
      if (!res.ok) throw redirect("/login");
      return res.json();
    },
    element: <Root />,
    children: [
      {
        path: "todos",
        id: "todos",
        loader: async () => {
          return fetch("/api/todos").then(r => r.json());
        },
        element: <Todos />,
        action: async ({ request }) => {
          const form = await request.formData();
          await fetch("/api/todos", {
            method: "POST",
            body: form,
          });
          return redirect("/todos");
        },
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

```

### Using Fetchers for Non-Navigating Mutations

Fetchers run actions without changing the URL, reusing the same data strategy pipeline:

```tsx
import { useFetcher } from "react-router";

export function AddTodo() {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method="post" action="/todos">
      <input name="title" />
      <button type="submit">Add</button>
    </fetcher.Form>
  );
}

```

### Custom Revalidation Logic

Attach `shouldRevalidate` hooks to routes to control when the data strategy engine reloads data:

```ts
export function shouldRevalidate({
  currentUrl,
  nextUrl,
  currentParams,
  nextParams,
}: ShouldRevalidateFunctionArgs) {
  return currentParams.projectId !== nextParams.projectId;
}

```

```tsx
{
  path: "project/:projectId",
  id: "project",
  loader: loadProject,
  shouldRevalidate,
  element: <Project />,
}

```

### Server-Side Rendering Implementation

The `StaticHandler` executes the same data strategy on the server:

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

export async function handleRequest(request: Request) {
  const staticHandler = createStaticHandler(routerRoutes);
  const context = await staticHandler.query(request);

  if (context instanceof Response) return context;

  return new Response(renderApp(context), {
    headers: { "Content-Type": "text/html" },
  });
}

```

## Summary

- **Three-layer architecture**: The data router combines a stateful `Router` instance ([`router.ts`](https://github.com/remix-run/react-router/blob/main/router.ts)), normalized `AgnosticDataRouteObject` routes ([`utils.ts`](https://github.com/remix-run/react-router/blob/main/utils.ts)), and a pluggable `DataStrategy` engine.
- **Centralized pipeline**: All navigation, fetchers, and revalidation flow through `startNavigation` → `completeNavigation`, ensuring consistent state updates.
- **Strategy-driven loading**: The `DataStrategyMatch` API with `shouldLoad` and `resolve()` determines exactly which routes fetch data, supporting lazy loading and custom middleware.
- **Universal execution**: `StaticHandler` runs the identical data strategy on the server as the client, guaranteeing consistent initial HTML and hydration.

## Frequently Asked Questions

### What is the primary purpose of the Data Strategy Engine in React Router?

The Data Strategy Engine isolates the logic that determines which routes need to load data during navigation. Implemented via `DataStrategyFunction` in [`packages/react-router/lib/router/utils.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts), it wraps each matched route in a `DataStrategyMatch` object that exposes `shouldLoad` and `resolve()` methods. This allows the router to lazy-load route modules and execute loaders only when necessary, while enabling developers to inject custom caching or middleware logic.

### How does React Router distinguish between client-side and server-side data loading?

Client-side navigation uses `createRouter` in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts), which maintains persistent state and listens to browser history events. Server-side rendering uses `createStaticHandler` (exposed via `react-router/server`), which creates a `StaticHandler` instance running the same data strategy but returning a `StaticHandlerContext` for one-time request handling. Both share the route matching and data execution logic but differ in state persistence and history management.

### What triggers a route revalidation in the data router?

Revalidation occurs when `router.revalidate()` is called, after successful action submissions, or during fetcher updates. The router creates a virtual navigation that re-runs the data strategy, checking each route's `shouldRevalidate` hook (or the default logic) to determine if fresh data is needed. This process respects the `unstable_defaultShouldRevalidate` flag and compares current versus next URL parameters to optimize network requests.

### How do fetchers interact with the main router state?

Fetchers are lightweight sub-routers that share the main router's data strategy pipeline but maintain isolated state slices for `loaderData` and `actionData`. When `router.fetch()` is invoked (lines ≈ 186‑200 in [`router.ts`](https://github.com/remix-run/react-router/blob/main/router.ts)), the fetcher runs through the same `DataStrategyMatch` resolution and `DataStrategyResult` aggregation as regular navigation, but without updating the browser URL or the main router's `location` state.