# How React Router's Internal Store Manages Route Data: A Deep Dive into RouterState

> Explore how React Router's internal store uses an immutable RouterState object to manage route data, updated on navigation and accessed via hooks like useLoaderData.

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

---

**React Router maintains route data in a private `RouterState` object that lives inside each `Router` instance, updated immutably during every navigation and exposed to components through hooks like `useLoaderData()` and `useFetcher()`.**

Unlike Redux or Zustand, React Router does not expose a public "store" for application state. Instead, the `remix-run/react-router` codebase implements a specialized internal state container that manages loader results, action submissions, fetcher states, and navigation errors. This `RouterState` object serves as the single source of truth for all routing concerns.

## What Is React Router's Internal Store?

React Router's internal store is the `RouterState` interface 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). Each `Router` instance created via `createBrowserRouter()` or `createMemoryRouter()` maintains its own private `state` property that conforms to this interface.

The store is intentionally isolated from React's component state to prevent unnecessary re-renders during complex navigation transitions. Instead, the router uses a subscription model where `RouterProvider` and individual hooks register listeners via `router.subscribe()`, receiving updates only when `updateState()` publishes a new state object.

## The RouterState Data Structure

The internal store organizes route-related data into distinct maps and properties, each handling a specific aspect of the navigation lifecycle.

| State Property | Type | Purpose |
|----------------|------|---------|
| `loaderData` | `RouteData` | Stores successful loader results keyed by route ID |
| `actionData` | `RouteData \| null` | Contains the most recent action submission result |
| `errors` | `RouteData \| null` | Maps error boundaries to thrown errors |
| `fetchers` | `Map<string, Fetcher>` | Active fetcher instances and their states |
| `blockers` | `Map<string, Blocker>` | Navigation blockers preventing history changes |

### Loader Data Storage

Loader results live in `state.loaderData`, a plain JavaScript object where keys correspond to route IDs defined in your route configuration. When a navigation initiates, the router does not immediately clear existing loader data. Instead, `mergeLoaderData()` (defined at lines 13-44 in [`router.ts`](https://github.com/remix-run/react-router/blob/main/router.ts)) combines new results with existing state, preserving data for routes that did not participate in the current navigation.

### Action Data and Errors

Action submissions follow a similar pattern but use separate storage. The `actionData` property receives results via `getActionDataForCommit()` during `completeNavigation()`. Errors are stored in the `errors` map, with `processRouteLoaderData()` (lines 53-66) bubbling the first encountered error up to the nearest error boundary route.

## How Route Data Flows Through the Store

React Router updates its internal store through a deterministic, immutable pipeline during every navigation.

### 1. Navigation Initialization

When `navigate()` is called or a POP event fires, the router creates a new `Location` object and invokes `startNavigation()`. This initializes the pending navigation state but does not yet modify `loaderData` or `actionData`.

### 2. Loader and Action Execution

The router constructs a `DataStrategy` that invokes route loaders and actions. Results are collected as `DataResult` objects in a temporary `results` map. This phase runs asynchronously, allowing parallel data fetching across route branches.

### 3. Processing Results with processRouteLoaderData()

Once loaders complete, `processRouteLoaderData()` processes the `results` map. This function (located at lines 53-66 in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts)) performs three critical operations:

- Builds a fresh `loaderData` object with successful results keyed by route ID
- Identifies routes that threw errors and marks them with `ResetLoaderDataSymbol` to clear stale data
- Constructs an `errors` map that bubbles the first error to the appropriate error boundary

```typescript
// Simplified representation from router.ts
function processRouteLoaderData(
  matches: AgnosticDataRouteMatch[],
  results: Record<string, DataResult>,
  pendingActionResult?: PendingActionResult
): {
  loaderData: RouterState["loaderData"];
  errors: RouterState["errors"] | null;
} {
  // Processes each match, extracts data or errors,
  // and determines which routes need data reset
}

```

### 4. Merging State with mergeLoaderData()

Rather than replacing `loaderData` entirely, `completeNavigation()` calls `mergeLoaderData()` (lines 13-44) to combine new results with existing state. This preserves data for routes that did not reload during the navigation, enabling persistent UI state across transitions.

```typescript
// From router.ts lines 13-44
function mergeLoaderData(
  loaderData: RouteData,
  newLoaderData: RouteData,
  matches: AgnosticDataRouteMatch[],
  errors: RouteData | null | undefined
): RouteData {
  // Merges objects, handles ResetLoaderDataSymbol,
  // and ensures error boundaries receive undefined data
}

```

### 5. Notifying Subscribers via updateState()

Finally, `updateState()` (lines 55-63) replaces the old state object with the new one and notifies all subscribers. This immutable update pattern ensures React components receive consistent snapshots of route data.

```typescript
// router.ts lines 55-63
function updateState(newState: Partial<RouterState>, opts = {}) {
  state = { ...state, ...newState };
  // Notify subscribers like RouterProvider
  subscribers.forEach(subscriber => subscriber(state, opts));
}

```

## Loader Data vs. Fetcher Data

While both loaders and fetchers interact with the internal store, they use distinct storage mechanisms.

**Loader Data** resides in `state.loaderData` and is accessed via the `useLoaderData()` hook defined in [`packages/react-router/lib/hooks.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx). This data is route-scoped and persists across navigations for routes that do not reload.

**Fetcher Data** maintains independent state within `state.fetchers`, a `Map<string, Fetcher>` where each entry contains its own `state`, `data`, `formData`, and submission metadata. The `useFetcher()` hook reads from and writes to this map, allowing components to load or submit data without changing the URL or affecting global loader data.

## Error Handling and Data Reset Semantics

When a route loader throws an error, React Router's store implements specific cleanup logic to prevent stale data from persisting.

The `processRouteLoaderData()` function marks failed routes with `ResetLoaderDataSymbol` during error processing. When `mergeLoaderData()` encounters this symbol, it explicitly sets the route's entry to `undefined`, ensuring that UI components do not display outdated information after an error boundary catches the exception.

Errors themselves are stored in `state.errors` as a map keyed by route ID, with `findNearestBoundary()` determining which route component should render the error boundary based on the route hierarchy.

## Summary

- React Router's internal store is the private `RouterState` object owned by each `Router` instance, defined in [`packages/react-router/lib/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router.ts).
- **Loader data** persists in `state.loaderData` and merges incrementally via `mergeLoaderData()` to preserve data for routes that do not reload.
- **Action results** populate `state.actionData` during `completeNavigation()`, while **errors** bubble through `state.errors` using boundary detection.
- **Fetchers** maintain isolated state in `state.fetchers`, a `Map` allowing parallel data interactions without affecting global route state.
- The store updates immutably through `updateState()`, notifying subscribers like `RouterProvider` after every navigation completes.

## Frequently Asked Questions

### Does React Router use Redux or another external state management library?

No. React Router implements its own specialized state container called `RouterState` that lives inside each router instance. This internal store manages only routing-related data such as loader results, action submissions, and navigation errors, eliminating the need for external dependencies like Redux.

### How does React Router prevent stale loader data from appearing after navigation?

The router uses `mergeLoaderData()` to combine new loader results with existing state rather than replacing the entire object. This function preserves data for routes that did not participate in the current navigation. When a loader throws an error, `processRouteLoaderData()` marks that route with `ResetLoaderDataSymbol`, causing `mergeLoaderData()` to explicitly clear the stale entry.

### What is the difference between loader data and fetcher data in the internal store?

Loader data resides in `state.loaderData`, a plain object keyed by route ID that persists across navigations and is accessed via `useLoaderData()`. Fetcher data lives in `state.fetchers`, a `Map<string, Fetcher>` where each fetcher maintains its own independent state, data, and form submission metadata, allowing components to interact with data without changing the URL or affecting global loader state.

### Where does error handling occur in React Router's state management?

Errors are processed during the `processRouteLoaderData()` phase, which constructs an `errors` map that bubbles the first encountered error up to the nearest error boundary route. These errors are stored in `state.errors` and cleared during `completeNavigation()` when the error is resolved or the user navigates away.