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 lastAction(POP,PUSH, orREPLACE) emitted by the history implementationlocation: The currentLocationobject containing pathname, search, hash, state, and keymatches: An array ofAgnosticDataRouteMatchobjects produced bymatchRoutesinutils.tsinitialized: Boolean indicating whether the initial data load has completedrenderFallback: Controls whether a HydrateFallback should render during SSR hydrationnavigation: ANavigationobject describing in-flight navigation status (idle,loading, orsubmitting)loaderData,actionData,errors: Data returned from route loaders, mutations, and error boundariesfetchers: A map of active fetcher instances for non-navigation data loadingblockers: A map of navigation blockers managing pending navigation interruptionsrevalidation: Status flag (idleorloading) for background data revalidationrestoreScrollPositionandpreventScrollReset: 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.
Navigation Initialization via startNavigation
The startNavigation function orchestrates the transition preparation:
- Cancels any in-flight navigation using
AbortControllerto prevent race conditions - Preserves the current scroll position for potential restoration
- Calls
matchRoutes(defined inpackages/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
Requestobject and scopedRouterContextProviderfor the navigation - Routes mutation submissions to
handleActionand data fetching tohandleLoaders
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
RouterStatethroughupdateState - Notifies all
RouterSubscribercallbacks (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:
- Creates a fetcher entry in
state.fetchers - Executes loaders or actions through the same pipeline as standard navigation
- Maintains the fetcher's lifecycle state (
idle,loading, orsubmitting) in the map - 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} />}
</>
);
}
Navigation Blocking and Interception
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
RouterStateobject owned by acreateRouterinstance inpackages/react-router/lib/router/router.ts - The state machine processes navigation through
startNavigationandcompleteNavigation, ensuring atomic state updates - History events from
packages/react-router/lib/router/history.tstrigger the state machine, which coordinates route matching viamatchRoutesinutils.ts - Fetchers enable data loading without URL changes by managing entries in
state.fetcherswith independent lifecycle states - Blockers intercept navigation through
shouldBlockNavigationand theblockerFunctionsmap, storingBlockerBlockedentries until resolved - Scroll restoration is managed through
savedScrollPositionsand evaluated duringcompleteNavigationbased 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →