How React Router Manages Navigation State Internally: The Complete Architecture Guide

React Router centralizes navigation state in a mutable RouterState object owned by a router instance created via createRouter, which processes history events through a deterministic state machine in router.ts that coordinates actions, loaders, fetchers, and blockers before emitting updates to React subscribers.

Navigation state management in React Router revolves around a single router instance created by createRouter. According to the remix-run/react-router source code, this architecture isolates all navigation logic—including location tracking, data loading, and transition states—within a framework-agnostic core that communicates with React via a subscription model.

The RouterState Data Structure

The router maintains a comprehensive RouterState object that serves as the single source of truth for the entire navigation lifecycle. This mutable state container describes everything the UI needs to know about the current navigation context:

  • historyAction: The last Action (POP, PUSH, or REPLACE) emitted by the history implementation
  • location: The current Location object containing pathname, search, hash, state, and key
  • matches: An array of AgnosticDataRouteMatch objects produced by matchRoutes in utils.ts
  • initialized: Boolean indicating whether the initial data load has completed
  • renderFallback: Controls whether a HydrateFallback should render during SSR hydration
  • navigation: A Navigation object describing in-flight navigation status (idle, loading, or submitting)
  • loaderData, actionData, errors: Data returned from route loaders, mutations, and error boundaries
  • fetchers: A map of active fetcher instances for non-navigation data loading
  • blockers: A map of navigation blockers managing pending navigation interruptions
  • revalidation: Status flag (idle or loading) for background data revalidation
  • restoreScrollPosition and preventScrollReset: Flags controlling scroll restoration behavior

The Navigation State Machine

React Router implements a deterministic state machine in packages/react-router/lib/router/router.ts that processes all navigation events through distinct phases, ensuring predictable transitions between application states.

History Event Detection

When the underlying History implementation (browser, hash, or memory) emits a POP event, the router's listen callback within the initialize function invokes startNavigation with the new Location and history action. This abstraction allows the router to normalize behavior across different environments without direct DOM dependencies.

The startNavigation function orchestrates the transition preparation:

  • Cancels any in-flight navigation using AbortController to prevent race conditions
  • Preserves the current scroll position for potential restoration
  • Calls matchRoutes (defined in packages/react-router/lib/router/utils.ts) to convert the route configuration into an array of matches for the target location
  • Handles edge cases including hash-only changes, 404s, and lazy route discovery
  • Constructs a Request object and scoped RouterContextProvider for the navigation
  • Routes mutation submissions to handleAction and data fetching to handleLoaders

Completion via completeNavigation

When all asynchronous operations settle, completeNavigation executes the state transition:

  • Merges new loader data with existing cached data using mergeLoaderData
  • Updates the browser history (push or replace) except for POP-only revalidations
  • Emits a new RouterState through updateState
  • Notifies all RouterSubscriber callbacks (including <RouterProvider>), triggering React re-renders with fresh matches and data

Fetchers and Non-Navigation Data Loading

Fetchers enable data loading and submission without changing the URL, essential for optimistic UI and background updates. When router.fetch(key, routeId, href, opts) is called in packages/react-router/lib/router/router.ts, the router:

  1. Creates a fetcher entry in state.fetchers
  2. Executes loaders or actions through the same pipeline as standard navigation
  3. Maintains the fetcher's lifecycle state (idle, loading, or submitting) in the map
  4. Automatically cleans up fetcher entries when they return to idle
import { useFetcher } from "@remix-run/react";

function Comments() {
  const fetcher = useFetcher();

  return (
    <>
      <button onClick={() => fetcher.load("/comments?post=1")}>
        Load comments
      </button>
      {fetcher.state === "loading" && <p>Loading…</p>}
      {fetcher.data && <CommentsList comments={fetcher.data} />}
    </>
  );
}

The router supports blocking navigation through the shouldBlockNavigation function. When navigate is called, this function checks the blockerFunctions map. If a blocker exists, the router stores a BlockerBlocked entry in state and pauses navigation until the UI calls proceed or reset, powering features like usePrompt for unsaved form warnings.

import { useBlocker } from "@remix-run/react";

function FormPage() {
  useBlocker(
    ({ currentLocation, nextLocation }) => {
      return window.confirm("Leave the page?");
    },
    true // block on POP as well
  );
  return <Form>...</Form>;
}

Background Revalidation

The router.revalidate() method triggers background data refreshing without changing the current location. The process creates a deferred promise, aborts in-flight loads, sets state.revalidation to "loading", and calls startNavigation with startUninterruptedRevalidation. Upon completion, completeNavigation clears the flag and resolves the promise, allowing UI components to display loading indicators while preserving current route content.

Scroll Restoration Management

Scroll restoration is handled through enableScrollRestoration, which injects callbacks that store positions in savedScrollPositions. When completeNavigation runs, getSavedScrollPosition determines whether to restore, keep, or ignore saved positions based on the navigation type and preventScrollReset flags. The router never manipulates the DOM directly; instead, it computes scroll targets and lets React components execute the actual scroll behavior.

Programmatic Navigation Examples

All navigation flows through the same state machine, whether triggered by user interaction or imperative code:

// Push a new entry
router.navigate("/about");

// Replace current entry
router.navigate("/settings", { replace: true });

// Go back (POP)
router.navigate(-1);

Inspecting internal state directly:

router.subscribe(state => {
  console.log('Current location:', state.location.pathname);
  console.log('Matches:', state.matches.map(m => m.route.id));
});

Key Implementation Files

File Role
packages/react-router/lib/router/router.ts Core router implementation, state machine, startNavigation, completeNavigation, fetcher and blocker logic
packages/react-router/lib/router/history.ts History abstraction (Action, Location, History interface) and concrete implementations (createBrowserHistory, createMemoryHistory, createHashHistory)
packages/react-router/lib/router/utils.ts Route matching via matchRoutes, route conversion utilities, data strategy helpers
packages/react-router/lib/router/links.ts URL construction utilities including createHref and resolveTo

Summary

  • React Router stores all navigation state in a mutable RouterState object owned by a createRouter instance in packages/react-router/lib/router/router.ts
  • The state machine processes navigation through startNavigation and completeNavigation, ensuring atomic state updates
  • History events from packages/react-router/lib/router/history.ts trigger the state machine, which coordinates route matching via matchRoutes in utils.ts
  • Fetchers enable data loading without URL changes by managing entries in state.fetchers with independent lifecycle states
  • Blockers intercept navigation through shouldBlockNavigation and the blockerFunctions map, storing BlockerBlocked entries until resolved
  • Scroll restoration is managed through savedScrollPositions and evaluated during completeNavigation based on navigation type and flags

Frequently Asked Questions

What is the difference between loaderData and actionData in RouterState?

loaderData stores the results from route loaders executed during navigation or revalidation, providing data for rendering routes. actionData contains the return value from the most recent form submission or action invocation, typically used to display mutation results or optimistic UI updates immediately after a submission.

How does React Router handle concurrent navigation requests?

The router maintains an AbortController that cancels in-flight navigation and data loading when a new navigation starts via startNavigation. This ensures that only the latest navigation completes its lifecycle, preventing race conditions and stale data updates when users rapidly click navigation links.

Can navigation state persist across browser sessions?

While the RouterState itself is ephemeral and maintained in memory, React Router persists navigation keys and state through the History API's location.state and location.key properties. For full state persistence across sessions, implement custom serialization in the history configuration or use session storage with custom scroll restoration logic.

Where does the actual DOM interaction happen if router.ts never touches the DOM?

The router emits state updates through updateState and subscribe callbacks that notify <RouterProvider>. React components then handle DOM operations such as scroll management through useScrollRestoration or navigation via <Link> components, keeping the router platform-agnostic and testable outside the browser environment.

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 →