Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
The router state reducer manages asynchronous client-side navigations, state patches, and history traversals for the Next.js App Router by maintaining a centralized action queue and dispatch switchboard. It processes incoming actions—such as client navigations, server-driven patches, page refreshes, hot-module reloads, server actions, and history restorations—to update the global application state, coordinate segment cache invalidations, and synchronize browser history entries. By decoupling the router state from React and coordinating transitions through specialized reducer modules, the system ensures reliable tree reconciliation, scroll and focus management, and seamless partial prerendering (PPR) support across user interactions.
Sources: packages/next/src/client/components/router-reducer/router-reducer.ts:23-58, packages/next/src/client/components/router-reducer/router-reducer-types.ts:203-250, packages/next/src/client/components/app-router-instance.ts:95-144
The Action Dispatch Architecture handles incoming navigation intents, history restorations, and data mutations by routing them through useActionQueue and scheduling them in an AppRouterActionQueue. Because the app router state lives outside React, actions are queued sequentially and dispatched to the main clientReducer switchboard, which delegates to specialized reducers based on action types.
Sources: packages/next/src/client/components/app-router-instance.ts:44-144, packages/next/src/client/components/router-reducer/router-reducer.ts:23-58, packages/next/src/client/components/use-action-queue.ts:12-16
Actions flow through a sequence of functions that manage asynchronous execution and queue priority. When an action is dispatched, dispatchAction() evaluates the current queue state:
dispatchAction() creates a deferred promise for asynchronous actions (unless the action type is ACTION_RESTORE) and constructs an ActionQueueNode.actionQueue.pending is null, the action runs immediately via runAction().ACTION_NAVIGATE or ACTION_RESTORE, the current pending action is marked as discarded = true, its .next pointer is preserved, and the navigation starts immediately via runAction().actionQueue.last and scheduled for execution after preceding actions finish.runAction() executes actionQueue.action(prevState, payload), handling promises and invoking handleResult() or runRemainingActions().runRemainingActions() advances actionQueue.pending to the next node in the queue and triggers the next action, or checks if actionQueue.needsRefresh is set to dispatch an ACTION_REFRESH.Warning
Navigations and restore actions (ACTION_NAVIGATE or ACTION_RESTORE) take immediate precedence over pending background actions by setting actionQueue.pending.discarded = true. Discarded actions that revalidated data will automatically trigger a deferred refresh via actionQueue.needsRefresh once remaining actions complete.
Sources: packages/next/src/client/components/app-router-instance.ts:113-127, packages/next/src/client/components/app-router-instance.ts:191-198
The clientReducer function acts as the central router switchboard, matching incoming action.type strings against known action constants and delegating to specialized reducer functions. If environment checks or unknown action types are encountered, specific branches or errors are thrown.
Note
On the server side, reducer evaluates to serverReducer, which is a noop function that immediately returns the incoming state unchanged, enabling better tree-shaking for server-side bundles.
Client-side router state initialization begins with createInitialRouterState, which processes the InitialRSCPayload delivered during server-side rendering or initial document load. This function extracts payload fields such as canonical URL parts, Flight data, rendered search queries, prefetch streams, and dynamic stale times to construct the initial AppRouterState.
The construction pipeline proceeds through a series of deterministic steps to establish the initial route tree and cache node structure:
createInitialRouterState() destructures InitialRSCPayload and normalizes the initial canonical URL and Flight data parts via getFlightDataPartsFromPath.convertRootFlightRouterStateToRouteTree() converts the initial FlightRouterState into a RouteTree, tracking metadata vary paths.createInitialCacheNodeForHydration() builds the initial cache node hierarchy using the route tree, seed data, head, and computed dynamic stale time.discoverKnownRoute() is invoked if running in the browser with a valid metadata vary path, registering the route pattern for future navigation prediction.Note
For statically generated HTML pages, the FlightRouterState baked into the initial RSC payload may omit correct segment inlining hints. The server marks these trees with InliningHintsStale, causing the route cache entry to expire immediately so that subsequent prefetches fetch correct hints from the /_tree endpoint.
When running in the browser (location !== null), the initialization routine populates the segment cache depending on whether the page is partially or fully static:
initialStaticStageByteLength and initialFlightStreamForCache are available, the Flight stream is cloned, truncated at the static stage byte boundary, decoded via decodeStageUntilBoundary, and cached using writePrerenderResponseIntoCache with FetchStrategy.PPR.writePrerenderResponseIntoCache, and the unused stream is cancelled.initialRuntimePrefetchStream is present, processRuntimePrefetchStream decodes the stream and writes runtime data into the cache under FetchStrategy.PPRRuntime.Warning
The initial hydration payload is treated as a complete, self-sufficient snapshot for rendering the page. The router deliberately avoids fetching missing data during initialization to preserve a reliable recovery path via full document reloads.
Client navigation processing begins inside navigateReducer, which acts as the entry point for handling NavigateAction payloads from user interactions or programmatic navigation calls. The reducer performs early validation checks—intercepting external URLs and page redirect meta tags to trigger hard Multi-Page Application (MPA) navigations via completeHardNavigation—before delegating internal routing tasks to segment cache navigation handlers.
Sources: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts:23-41, packages/next/src/client/components/segment-cache/navigation.ts:620-653
Sources: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts:23-56, packages/next/src/client/components/segment-cache/navigation.ts:600-618
Internal navigations processed through navigateUsingSegmentCache resolve prefetch trees, construct target route states, and coordinate Partial Prerendering (PPR) tasks. Once target cache nodes and FlightRouterState trees are computed, the navigation concludes by invoking completion routines.
navigateReducer() → navigateUsingSegmentCache() (located in segment-cache navigation) → completeSoftNavigation() → constructs final AppRouterState.completeSoftNavigation evaluates path changes for interception routes via computeChangedPath, detects hash-only URL modifications, computes scroll targets, and manages scroll reference invalidation across pending navigations.Sources: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts:23-56, packages/next/src/client/components/segment-cache/navigation.ts:600-618, packages/next/src/client/components/segment-cache/navigation.ts:655-791
Note
During soft navigations, if a user opts out of scrolling (scroll={false}), any newly created per-node scroll reference is neutralized by setting scrollRef.current = false, while prior active scroll references carried forward on cache nodes remain intact.
When building route trees for navigation, abstract route patterns are translated into concrete instances through reifyRouteTree, which substitutes dynamic segment values from resolved parameters and computes vary paths to key segment cache entries correctly.
function reifyRouteTree(
pattern: RouteTree,
resolvedParams: ResolvedParams,
search: NormalizedSearch,
parentPartialVaryPath: PartialSegmentVaryPath | null,
acc: ReifyAccumulator
): RouteTreeParallel slots and page segments are traversed recursively. For page nodes, vary paths incorporate request keys and search parameters, whereas layout segments finalize without search parameters.
Sources: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts:16-21, packages/next/src/client/components/segment-cache/navigation.ts:606, packages/next/src/client/components/segment-cache/navigation.ts:714-725
Caution
Javascript URLs (javascript:) passed to navigation actions are explicitly blocked and logged as security errors inside completeHardNavigation, immediately returning the unmodified router state.
When a route mismatch occurs or server-driven updates are received, the client router applies Flight router state patches to update active route subtrees and cache nodes. This reconciliation process is managed by serverPatchReducer, which validates whether the incoming server response matches the expected router state before executing a known route navigation with a refresh freshness policy.
The server patch reconciliation workflow delegates execution through specific functions depending on whether payload validation succeeds. The execution sequence follows:
serverPatchReducer() → checks action.mpa / action.seed → validates action.previousTree === state.tree → navigateToKnownRoute() (or falls back to completeHardNavigation() or refreshReducer()).
During tree traversal and reconciliation, utility functions inspect FlightRouterState structures to extract paths and parameters. The path extraction and tree differencing call-chain operates as:
computeChangedPath() → computeChangedPathImpl() → matches segments via matchSegment() → falls back to extractPathFromFlightRouterState() and normalizeSegments().
Note
If a more recent navigation has occurred since the mismatched patch was dispatched (action.previousTree !== state.tree), serverPatchReducer aborts the retry and invokes refreshReducer to evict stale dynamic data while preserving the latest navigation state.
The router state reducer handles several distinct action types governing server patches, refreshes, and navigation restoration, defined in the reducer type definitions.
Standard and hot-module reload (HMR) refreshes re-fetch dynamic RSC payload data for the current URL while coordinating segment cache invalidation. When a refresh action or an HMR update occurs, the reducer invalidates stale dynamic entries to ensure fresh content is displayed without throwing away the underlying route structure.
Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:23-39, packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts:8-10
The refresh workflow processes standard refreshes and HMR refreshes through a dedicated sequence of helper functions. The call-chain executes as follows:
refreshReducer() / hmrRefreshReducer() → refreshDynamicData() → invalidateBfCache() → hasInterceptionRouteInCurrentTree() → convertServerPatchToFullTree() → navigateToKnownRoute().
Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:19-104, packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts:8-10
refreshReducer checks whether testing flags bypass cache invalidation (process.env.__NEXT_EXPOSE_TESTING_API && action.bypassCacheInvalidation). If not bypassed, it invokes invalidateSegmentCacheEntries(currentNextUrl, currentRouterState).refreshReducer and hmrRefreshReducer delegate to refreshDynamicData(state, freshnessPolicy), passing either FreshnessPolicy.RefreshAll or FreshnessPolicy.HMRRefresh.refreshDynamicData clears the back/forward cache via invalidateBfCache(), then resolves nextUrlForRefresh by evaluating hasInterceptionRouteInCurrentTree(state.tree).refreshSeed is generated by calling convertServerPatchToFullTree(), and the refresh is finalized by invoking navigateToKnownRoute() with a 'replace' navigation type and ScrollBehavior.NoScroll.Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:31-103, packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts:8-10
Note
During a refresh, the router invalidates the segment cache (which holds dynamic RSC data) but deliberately leaves the route cache intact, because the underlying route tree structure does not change across a refresh.
Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:1-104, packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts:1-10
Server action execution and revalidation in the router reducer handles processing server action requests, parsing action Flight responses, resolving redirects, and updating cache state. When a server action is invoked, the action reducer extracts server reference info, encodes reply arguments, builds action headers including the router state tree, and executes the fetch request.
Sources: packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts:104-121
The processing of a server action flows through a specific sequence of operations:
fetchServerAction() parses and encodes action arguments using extractInfoFromServerReferenceId(), omitUnusedArgs(), and encodeReply().revalidationKind. If revalidation occurs (ActionDidRevalidateStaticAndDynamic), invalidateBfCache() and invalidateEntirePrefetchCache() are invoked, followed by startRevalidationCooldown().isExternalURL() determines whether to trigger an external MPA hard navigation via completeHardNavigation() or an internal SPA redirect with createRedirectErrorForAction().flightData !== undefined && flightDataRenderedSearch !== undefined), convertServerPatchToFullTree() generates a redirect seed, discoverKnownRoute() registers the route pattern, and navigateToKnownRoute() completes the transition.Sources: packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts:109-522
Warning
If a server action triggers a redirect without sending any Flight data, the router treats it as an external redirect and immediately forces a hard navigation via completeHardNavigation().
Sources: packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts:423-429
Sources: packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts:51-102, packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts:343-368
History traversal relies on the restoreReducer function to handle popstate events, reconstruct the target route state from history entries, and coordinate back/forward cache integration. When a user triggers browser navigation, the reducer inspects the incoming RestoreAction history state. If the history state lacks a valid FlightRouterState—such as for pre-hydration entries or anchor link hash navigations—it retains the existing tree via state.tree to prevent invalid router states. Otherwise, it extracts the restore tree, rendered search parameters, and canonical URL.
The restoration process follows an explicit execution sequence from state extraction through task spawning and tree traversal:
restoreReducer() — Receives state and RestoreAction, resolving treeToRestore from historyState.tree or falling back to state.tree.convertServerPatchToFullTree() — Takes the restored tree, current timestamp, and unknown dynamic stale times (UnknownDynamicStaleTime) to build a full NavigationSeed containing the route tree and vary paths.startPPRNavigation() — Evaluates the navigation task using FreshnessPolicy.HistoryTraversal, evaluating the segment cache against the restore seed route tree.task === null) — If the task creation fails, it falls back to a hard navigation via completeHardNavigation(state, restoredUrl, 'replace'). Otherwise, it proceeds to spawn dynamic requests.spawnDynamicRequests() — Dispatches background requests for dynamic data using the 'replace' navigate type and history traversal freshness policy.completeTraverseNavigation() — Finalizes the traversal update, returning a new AppRouterState with preserveCustomHistoryState set to true.Sources: packages/next/src/client/components/router-reducer/reducers/restore-reducer.ts:22-104, packages/next/src/client/components/segment-cache/navigation.ts:803-822
Warning
History traversal never uses route prediction. If a dynamic data mismatch occurs during a restore task, the retry handler must traverse the known route tree to locate and mark the mismatched entry.
When restoring state, helper utilities inspect router trees to determine pathnames and parameters. extractPathFromFlightRouterState() processes segment nodes, ignoring default segment keys and interception markers, while computeChangedPath() calculates differences between state trees during traversals.
Sources: packages/next/src/client/components/router-reducer/compute-changed-path.ts:81-118, packages/next/src/client/components/router-reducer/compute-changed-path.ts:208-220