Relevant source files
The following files were used as context for generating this wiki page:
- packages/next/src/client/components/router-reducer/router-reducer.ts
- packages/next/src/client/components/router-reducer/reducers/restore-reducer.ts
- packages/next/src/client/components/app-router-state.ts
- packages/next/src/client/components/app-router-instance.ts
- packages/next/src/client/components/router-reducer/router-reducer-types.ts
- packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
- packages/next/src/client/components/app-router.tsx
- packages/next/src/client/components/render-tree.ts
- packages/next/src/client/components/sequential-router-queue.ts
- packages/next/src/shared/lib/router/router.ts
- packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts
- packages/next/src/client/components/router-reducer/create-initial-router-state.ts
- packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts
- packages/next/src/next-devtools/dev-overlay/shared.ts
- packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts
- packages/next/src/client/components/segment-cache/decode-server-response.ts
- packages/next/src/client/components/use-action-queue.ts
- packages/next/src/client/components/segment-cache/cache.ts
- packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts
- packages/next/src/client/components/navigator.ts
- packages/next/src/client/components/router-reducer/reducers/committed-state.ts
- packages/next/src/client/components/router-reducer/fetch-server-response.ts
Overview
The router reducer is the centralized state engine for the client-side AppRouter, responsible for computing and transitioning the application's top-level router state across user actions, navigation events, and server responses. Operating through a deterministic dispatch loop, it handles incoming action payloads by routing them to specialized reducer functions such as navigateReducer, serverPatchReducer, restoreReducer, refreshReducer, hmrRefreshReducer, and serverActionReducer. By coordinating with the Client Runtime, Segment Cache, and action queues, the router reducer manages segment revalidations, history traversals, and state restoration without full page reloads, bridging the gap between browser history states and React component tree updates.
Sources: packages/next/src/client/components/router-reducer/router-reducer.ts:20-58, packages/next/src/client/components/router-reducer/router-reducer-types.ts:204-258
Action Dispatch and Reducer Routing
Dispatch Loop and Reducer Routing
The client-side state engine processes incoming reducer actions through clientReducer, a deterministic dispatch function that examines action.type and delegates payload handling to specialized reducer modules App Router. When operating on the server (typeof window === 'undefined'), the router executes serverReducer, a no-op function that returns the state unchanged to optimize server bundle tree-shaking Client Runtime.
function clientReducer(
state: ReadonlyReducerState,
action: ReducerActions
): ReducerState {
switch (action.type) {
case ACTION_NAVIGATE: {
return navigateReducer(state, action)
}
case ACTION_SERVER_PATCH: {
return serverPatchReducer(state, action)
}
case ACTION_RESTORE: {
return restoreReducer(state, action)
}
case ACTION_REFRESH: {
return refreshReducer(state, action)
}
case ACTION_HMR_REFRESH: {
if (process.env.NODE_ENV === 'development') {
const { hmrRefreshReducer } =
require('./reducers/hmr-refresh-reducer') as typeof import('./reducers/hmr-refresh-reducer')
return hmrRefreshReducer(state, action)
} else {
throw new Error(
'hmrRefresh can only be used in development mode. Please use refresh instead.'
)
}
}
case ACTION_SERVER_ACTION: {
return serverActionReducer(state, action)
}
default:
throw new Error('Unknown action')
}
}Sources: packages/next/src/client/components/router-reducer/router-reducer.ts:23-58
The supported action constants define the complete mutation surface handled by the routing subsystem. Each action type maps to a distinct navigation or update lifecycle event.
| Action Constant | String Value | Target Reducer / Handler | Purpose |
|---|---|---|---|
ACTION_REFRESH | 'refresh' | refreshReducer | Full page data refresh and root state update |
ACTION_NAVIGATE | 'navigate' | navigateReducer | Push or replace navigation to a target URL |
ACTION_RESTORE | 'restore' | restoreReducer | History popstate traversal or state sync |
ACTION_SERVER_PATCH | 'server-patch' | serverPatchReducer | Applying server Flight data patches to cache and tree |
ACTION_HMR_REFRESH | 'hmr-refresh' | hmrRefreshReducer | Development-only HMR boundary refresh |
ACTION_SERVER_ACTION | 'server-action' | serverActionReducer | Server Action dispatch, execution, and response serialization |
Sources: packages/next/src/client/components/router-reducer/router-reducer-types.ts:8-13, packages/next/src/client/components/router-reducer/router-reducer.ts:27-53
Note
The ACTION_HMR_REFRESH case dynamically requires hmrRefreshReducer only when process.env.NODE_ENV === 'development'. In production builds, triggering this action throws an explicit error instructing callers to use refresh instead.
Sources: packages/next/src/client/components/router-reducer/router-reducer.ts:40-50
Operation Interface and Sequential Queue Coordination
User-facing operations originate in Navigator, which establishes safety preconditions and wraps dispatches inside React transitions before forwarding actions to the sequential action queue. The navigator exposes a unified function set (navigate, push, replace, traverse, restore, refresh, and hmrRefresh) implemented by sequential-router-queue.ts.
export function navigate(
href: string,
navigateType: NavigateAction['navigateType'],
scrollBehavior: ScrollBehavior,
linkInstanceRef: LinkInstance | null,
transitionTypes: string[] | undefined,
prefetchIntent: RouterTransitionPrefetchIntent | null
): void {
if (isJavaScriptURLString(href)) {
throw new Error(
'Next.js has blocked a javascript: URL as a security precaution.'
)
}
startTransition(() => {
if (transitionTypes) {
for (const type of transitionTypes) {
addTransitionType(type)
}
}
const url = new URL(addBasePath(href), location.href)
if (process.env.__NEXT_APP_NAV_FAIL_HANDLING) {
window.next.__pendingUrl = url
}
setLinkForCurrentNavigation(linkInstanceRef)
startRouterTransition(
href,
navigateType,
getRequiredAppRouterState().tree,
prefetchIntent
)
dispatchAppRouterAction({
type: ACTION_NAVIGATE,
url,
isExternalUrl: isExternalURL(url),
locationSearch: location.search,
scrollBehavior,
navigateType,
})
})
}Sources: packages/next/src/client/components/sequential-router-queue.ts:47-93
Warning
Security checks inside navigate block any javascript: URL strings immediately before starting transitions, throwing an error to prevent script injection vulnerabilities Client Runtime.
Sources: packages/next/src/client/components/sequential-router-queue.ts:55-59
Development-mode HMR refreshes manage an active AbortController generation tracker when __NEXT_SERVER_COMPONENTS_HMR_CANCELLATION is enabled. This tears down superseded fetch requests prior to dispatching new HMR refresh actions.
export function hmrRefresh(): void {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'hmrRefresh can only be used in development mode. Please use refresh instead.'
)
} else {
resetKnownRoutes()
let signal: AbortSignal | undefined
if (process.env.__NEXT_SERVER_COMPONENTS_HMR_CANCELLATION) {
activeHmrRefreshController?.abort()
activeHmrRefreshController = new AbortController()
signal = activeHmrRefreshController.signal
}
startTransition(() => {
dispatchAppRouterAction({
type: ACTION_HMR_REFRESH,
signal,
})
})
}
}Sources: packages/next/src/client/components/sequential-router-queue.ts:168-192
Initial Router State Hydration
Segment Route Trees and Seed Decoding
Client hydration bootstraps the application router state by extracting parameters from InitialRSCPayload App Router. The initial transport tree is converted into a flight router state using transportNodeToFlightRouterState, and the canonical URL is resolved from browser location or parsed parts.
export function createInitialRouterState({
navigatedAt,
initialRSCPayload,
initialFlightStreamForCache,
location,
}: InitialRouterStateParameters): AppRouterState {
const {
c: initialCanonicalUrlParts,
t: initialTransportData,
q: initialRenderedSearch,
i: initialCouldBeIntercepted,
S: initialSupportsPerSegmentPrefetching,
s: initialStaleTime,
l: initialStaticStageByteLength,
r: initialRootVaryParams,
p: initialRuntimePrefetchStream,
d: initialDynamicStaleTimeSeconds,
} = initialRSCPayload
// When initialized on the server, the canonical URL is provided as an array of parts.
// This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it
// as a URL that should be crawled.
const initialCanonicalUrl = initialCanonicalUrlParts.join('/')
// The initial router state tree, derived from the transport tree.
const initialTree = transportNodeToFlightRouterState(
initialTransportData.t,
initialRenderedSearch
)
Sources: packages/next/src/client/components/router-reducer/create-initial-router-state.ts:28-57
createNavigationSeed decodes initial transport data into a RouteTree structure with embedded render outputs and heads. When executing in the browser (location !== null), the client registers known routes via discoverKnownRoute and schedules segment cache population based on the stale time configuration.
const initialSeed = createNavigationSeed(
navigatedAt,
// There's no base tree to overlay onto; the initial payload is a full
// render from the root.
null,
initialTransportData,
// The initial payload may still be streaming in while we hydrate, so its
// vary params can't be drained here; they decode as null. The
// segment-cache write below re-decodes the transport data with the
// payload's root params once the stale time has resolved.
null,
// Same for partiality: only segment-cache writes consume it, and the
// write below re-decodes with the payload's actual response-level value.
// Pass the conservative value here.
true,
// The initial payload always includes the param values in the tree
// (fallback shells are patched with the parsed values before this runs —
// see createInitialRSCPayloadFromFallbackPrerender), so there's no
// pathname to parse them from.
null,
initialRenderedSearch,
null,
initialDynamicStaleTimeSeconds ?? UnknownDynamicStaleTime
)
const initialRoot = initialSeed.root
const initialNavigation = createInitialRenderTreeForHydration(
navigatedAt,
initialRoot,
initialSeed.dynamicStaleAt
)
// The following only applies in the browser (location !== null) since neither
// route learning nor segment cache state persists from SSR to client.
if (location !== null) {
// Learn the route pattern so we can predict it for future navigations.
discoverKnownRoute(
Date.now(),
location.pathname,
location.search as NormalizedSearch,
null, // nextUrl — initial render is never an interception
null, // No pending entry
initialRoot,
initialCouldBeIntercepted,
canonicalUrl,
initialSeed.renderedSearch,
initialSupportsPerSegmentPrefetching,
false // hasDynamicRewrite
)Sources: packages/next/src/client/components/router-reducer/create-initial-router-state.ts:77-124
Note
For statically generated build-time HTML pages, trees baked into the initial RSC payload may lack correct segment inlining hints. The server marks these with InliningHintsStale, expiring the route cache entry so subsequent prefetches retrieve corrected hints from /_tree.
Sources: packages/next/src/client/components/router-reducer/create-initial-router-state.ts:72-76
Static and Runtime Stream Caching
Depending on whether initialStaticStageByteLength is present alongside initialFlightStreamForCache, the hydration logic branches between partially static and fully static payloads to populate segment caches Segment Cache.
| Payload Condition | Byte Length & Stream Checks | Caching Behavior |
|---|---|---|
| Partially Static Page | initialStaticStageByteLength !== undefined && initialFlightStreamForCache != null | Truncates cloned Flight stream at the static stage boundary, decodes via decodeStageUntilBoundary, and writes via spawnStaticStageCacheWrite. |
| Fully Static Page | initialStaticStageByteLength === undefined | Synthesizes a transport subset (t, r, s) directly from already decoded hydration data and writes to cache without re-decoding. |
| No Caching / Unused | initialStaleTime === undefined | Cancels initialFlightStreamForCache immediately. |
if (initialStaleTime !== undefined) {
if (
initialStaticStageByteLength !== undefined &&
initialFlightStreamForCache != null
) {
// Partially static page — truncate the cloned Flight stream at the
// static stage byte boundary, decode, and cache the static subset.
// Promise.resolve wraps the Flight-deserialized thenable into a
// native Promise so we can chain `.then` on it safely.
Promise.resolve(initialStaticStageByteLength)
.then(async (byteLength) => {
if (byteLength === 0) {
initialFlightStreamForCache.cancel()
return
}
const staticStageResponse =
await decodeStageUntilBoundary<InitialRSCPayload>(
initialFlightStreamForCache,
byteLength,
undefined
)
if (staticStageResponse === null) {
return
}
spawnStaticStageCacheWrite(
Date.now(),
staticStageResponse,
true, // isResponsePartial
null, // responseHeaders — no build-id check for initial HTML
initialTree,
initialRenderedSearch,
segmentCacheMap // hydration writes are bound to the shared map
)
})
.catch(() => {
// The static stage processing failed. Not fatal — the page
// rendered normally, we just won't write into the cache.
})
} else {
// Fully static page — cache the initial payload's segment data as-is.
// We're not using the initial response here (which would allow us to
// combine the two branches) to avoid unnecessary decoding of the
// Flight data, since we can just take the segment data that we
// already decoded during hydration and write it into the
// cache directly.
spawnStaticStageCacheWrite(
Date.now(),
// The transport subset of the initial payload, already decoded
// during hydration. `u` (the runtime-data verdict) is deliberately
// omitted from this synthesized subset — its writes record their
// strategy unrefined — while the truncated branch above forwards
// the decoded payload's own `u`.
{
t: initialTransportData,
r: initialRootVaryParams,
s: initialStaleTime,
},
false, // isResponsePartial
null, // responseHeaders — no build-id check for initial HTML
initialTree,
initialRenderedSearch,
segmentCacheMap // hydration writes are bound to the shared map
)
// Cancel the stream clone — fully static path doesn't need it.
initialFlightStreamForCache?.cancel()
}
} else {
// No caching — cancel the unused stream clone.
initialFlightStreamForCache?.cancel()
}
Sources: packages/next/src/client/components/router-reducer/create-initial-router-state.ts:133-204
Warning
Hydration writes are strictly bound to segmentCacheMap. If runtime prefetch streams are included (initialRuntimePrefetchStream != null), writeRuntimePrefetchStreamIntoCache is invoked asynchronously during initialization but failures remain non-fatal.
Sources: packages/next/src/client/components/router-reducer/create-initial-router-state.ts:209-221
Navigation Transitions and Route Trees
Handling Client Navigation Actions
The navigateReducer processes incoming NavigateAction payloads App Router. It checks whether the target URL is external or if a meta page redirect tag (__next-page-redirect) is present in the document. When either condition matches, it immediately triggers a hard multi-page application navigation using completeHardNavigation.
export function navigateReducer(
state: ReadonlyReducerState,
action: NavigateAction
): ReducerState {
const { url, isExternalUrl, navigateType, scrollBehavior } = action
if (isExternalUrl) {
return completeHardNavigation(state, url, navigateType)
}
// Handles case where `<meta http-equiv="refresh">` tag is present,
// which will trigger an MPA navigation.
if (document.getElementById('__next-page-redirect')) {
return completeHardNavigation(state, url, navigateType)
}
Sources: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts:23-38
When processing standard soft navigations, navigateReducer extracts the current URL from state.canonicalUrl and delegates to navigate from app-router-state.ts with FreshnessPolicy.Default Segment Cache.
// Temporary glue code between the router reducer and the new navigation
// implementation. Eventually we'll rewrite the router reducer to a
// state machine.
const currentUrl = new URL(state.canonicalUrl, location.origin)
const currentRenderedSearch = state.renderedSearch
return navigateUsingSegmentCache(
state,
url,
currentUrl,
currentRenderedSearch,
state.root,
state.tree,
state.nextUrl,
FreshnessPolicy.Default,
scrollBehavior,
navigateType
)
}Sources: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts:39-56
Warning
Security checks block any javascript: URL strings during hard navigation resolution, logging an error and returning the current router state unmodified without navigating.
Sources: packages/next/src/client/components/app-router-state.ts:630-636
Stale Time Verification and Constants
Stale time parameters govern how long cached route data and segment responses remain valid before requiring revalidation. These constants are initialized from environment configuration variables defined by define-env-plugin based on nextConfig.experimental.staleTimes.
| Constant Name | Source Expression | Default Behavior |
|---|---|---|
DYNAMIC_STALETIME_MS | Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000 | Defaults to 0 seconds for dynamic routes. |
STATIC_STALETIME_MS | getStaleTimeMs(Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME)) | Defaults to 5 minutes for static routes. |
export const DYNAMIC_STALETIME_MS =
Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000
export const STATIC_STALETIME_MS = getStaleTimeMs(
Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME)
)Sources: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts:16-21
Transitioning Router State Between Route Trees
When executing navigate, the router checks if testing APIs are enabled via process.env.__NEXT_EXPOSE_TESTING_API. If navigation locking is active, beginLockedNavigation creates a NavigationLock and forces an initial prefetch validation step before continuing.
if (process.env.__NEXT_EXPOSE_TESTING_API) {
const { isNavigationLocked } =
require('./segment-cache/navigation-testing-lock') as typeof import('./segment-cache/navigation-testing-lock')
if (isNavigationLocked()) {
// Signal that a new locked navigation is starting. This force-resolves the
// previous locked navigation's withheld data (so a reused shared segment
// no longer carries a pending deferred rsc) and returns this navigation's
// own withheld-data gate.
navigationLock = beginLockedNavigation()
return ensurePrefetchThenNavigate(
state,
url,
currentUrl,
currentRenderedSearch,
currentRoot,
currentFlightRouterState,
nextUrl,
freshnessPolicy,
scrollBehavior,
navigateType,
navigationLock
)
}
}Sources: packages/next/src/client/components/app-router-state.ts:81-104
navigateImpl evaluates cache keys using createCacheKey and reads route cache entries via readRouteCacheEntry. If a fulfilled entry is found, it calls navigateUsingPrefetchedRouteTree. Otherwise, if optimistic routing is disabled (process.env.__NEXT_OPTIMISTIC_ROUTING), it attempts to construct an optimistic route entry using deprecated_requestOptimisticRouteCacheEntry.
function navigateImpl(
state: AppRouterState,
url: URL,
currentUrl: URL,
currentRenderedSearch: string,
currentRoot: RootRouteTree<CacheNode>,
currentFlightRouterState: FlightRouterState,
nextUrl: string | null,
freshnessPolicy: FreshnessPolicy,
scrollBehavior: ScrollBehavior,
navigateType: 'push' | 'replace',
navigationLock: NavigationLock | null,
// The segment cache map this navigation is bound to: a locked navigation's
// driving-task map, or the shared map. See `segmentCacheMap` in cache.ts.
map: CacheMap<SegmentCacheEntry>
): AppRouterState | Promise<AppRouterState> {
const now = Date.now()
const href = url.href
const cacheKey = createCacheKey(href, nextUrl)
const route = readRouteCacheEntry(now, cacheKey)
if (route !== null && route.status === EntryStatus.Fulfilled) {
// We have a matching prefetch.
return navigateUsingPrefetchedRouteTree(
now,
state,
url,
currentUrl,
currentRenderedSearch,
nextUrl,
currentRoot,
freshnessPolicy,
scrollBehavior,
navigateType,
route,
navigationLock,
map
)
}
// There was no matching route tree in the cache. Let's see if we can
// construct an "optimistic" route tree using the deprecated search-params
// based matching. This is only used when the new optimisticRouting flag is
// disabled.
//
// Do not construct an optimistic route tree if there was a cache hit, but
// the entry has a rejected status, since it may have been rejected due to a
// rewrite or redirect based on the search params.
//
// TODO: There are multiple reasons a prefetch might be rejected; we should
// track them explicitly and choose what to do here based on that.
if (!process.env.__NEXT_OPTIMISTIC_ROUTING) {
if (route === null || route.status !== EntryStatus.Rejected) {
const optimisticRoute = deprecated_requestOptimisticRouteCacheEntry(
now,
url,
nextUrl
)
if (optimisticRoute !== null) {Sources: packages/next/src/client/components/app-router-state.ts:123-181
During soft navigation completion in completeSoftNavigation, the router computes path alterations between the previous tree and incoming navigation tree using computeChangedPath to update interception parameters (nextUrl and previousNextUrl).
export function completeSoftNavigation(
oldState: AppRouterState,
url: URL,
referringNextUrl: string | null,
navigation: RootNavigationTask,
renderedSearch: string,
canonicalUrl: string,
navigateType: 'push' | 'replace',
scrollBehavior: ScrollBehavior,
scrollRef: ScrollRef | null,
collectedDebugInfo: Array<unknown> | null
) {
// The "Next-Url" is a special representation of the URL that Next.js
// uses to implement interception routes.
// TODO: Get rid of this extra traversal by computing this during the
// same traversal that computes the tree itself. We should also figure out
// what is the minimum information needed for the server to correctly
// intercept the route.
const tree = navigation.tree.route
const changedPath = computeChangedPath(oldState.tree, tree)
const nextUrlForNewRoute = changedPath ? changedPath : oldState.nextUrl
// This value is stored on the state as `previousNextUrl`; the naming is
// confusing. What it represents is the "Next-Url" header that was used to
// fetch the incoming route. It's essentially the refererer URL, but in a
// Next.js specific format. During refreshes, this is sent back to the server
// instead of the current route's "Next-Url" so that the same interception
// logic is applied as during the original navigation.
const previousNextUrl = referringNextUrl
Sources: packages/next/src/client/components/app-router-state.ts:660-689
Tip
completeSoftNavigation inspects whether an incoming navigation constitutes solely a hash fragment modification by comparing pathnames and query search parameters against the old URL instance.
Sources: packages/next/src/client/components/app-router-state.ts:690-700
Cache Invalidation and Refresh Operations
Segment Caching and Refresh Reducer Execution
When refreshReducer executes, it handles general refresh actions by clearing segment entries via invalidateSegmentCacheEntries unless bypassed by testing flags, while preserving the structural route cache. It then invokes refreshDynamicData using FreshnessPolicy.RefreshAll to re-fetch dynamic RSC data.
export function refreshReducer(
state: ReadonlyReducerState,
action: RefreshAction
): ReducerState {
// During a refresh, we invalidate the segment cache but not the route cache.
// The route cache contains the tree structure (which segments exist at a
// given URL) which doesn't change during a refresh. The segment cache
// contains the actual RSC data which needs to be re-fetched.
//
// The Instant Navigation Testing API can bypass cache invalidation to
// preserve prefetched data when refreshing after an MPA navigation. This is
// only used for testing and is not exposed in production builds by default.
const bypassCacheInvalidation =
process.env.__NEXT_EXPOSE_TESTING_API && action.bypassCacheInvalidation
if (!bypassCacheInvalidation) {
const currentNextUrl = state.nextUrl
const currentRoot = state.root
invalidateSegmentCacheEntries(currentNextUrl, currentRoot)
}
// A full refresh has no HMR generation to cancel.
return refreshDynamicData(state, FreshnessPolicy.RefreshAll, undefined)
}Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:20-41
Note
The Instant Navigation Testing API controlled by process.env.__NEXT_EXPOSE_TESTING_API and action.bypassCacheInvalidation allows skipping cache invalidation to preserve prefetched data during multi-page application (MPA) testing refreshes.
Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:29-38
Dynamic Data Re-fetching and Navigation Seeds
refreshDynamicData invalidates the back-forward cache (invalidateBfCache) and checks for interception routes within the tree using hasInterceptionRouteInCurrentTree to decide whether to reuse state.previousNextUrl or currentNextUrl. It builds a NavigationSeed via createNavigationSeed and executes navigateToKnownRoute with ScrollBehavior.NoScroll.
export function refreshDynamicData(
state: ReadonlyReducerState,
freshnessPolicy: FreshnessPolicy.RefreshAll | FreshnessPolicy.HMRRefresh,
signal: AbortSignal | undefined
): ReducerState {
// During a refresh, invalidate the BFCache, which may contain dynamic data.
invalidateBfCache()
const currentNextUrl = state.nextUrl
// We always send the last next-url, not the current when performing a dynamic
// request. This is because we update the next-url after a navigation, but we
// want the same interception route to be matched that used the last next-url.
const nextUrlForRefresh = hasInterceptionRouteInCurrentTree(state.tree)
? state.previousNextUrl || currentNextUrl
: null
// A refresh is modeled as a navigation to the current URL, but where any
// existing dynamic data (including in shared layouts) is re-fetched.
const currentCanonicalUrl = state.canonicalUrl
const currentUrl = new URL(currentCanonicalUrl, location.origin)
const currentRenderedSearch = state.renderedSearch
const currentFlightRouterState = state.tree
const scrollBehavior = ScrollBehavior.NoScroll
const navigationLock = getCurrentNavigationLock()
// Create a NavigationSeed from the current FlightRouterState.
// TODO: Eventually we will store this type directly on the state object
// instead of reconstructing it on demand. Part of a larger series of
// refactors to unify the various tree types that the client deals with.
const now = Date.now()
// TODO: Store the dynamic stale time on the top-level state so it's known
// during restores and refreshes.
const refreshSeed = createNavigationSeed(
now,
currentFlightRouterState,
// No transport data (and so no vary params, no partiality, and no
// pathname to parse params from) — this converts the base tree alone.
null,
null,
true,
null,
currentRenderedSearch,
null,
UnknownDynamicStaleTime
)
// If the previous navigation hasn't pushed its history entry yet (React
// hasn't committed its state), this refresh may commit in its place, so it
// takes over the push. If the navigation does commit first, HistoryUpdater
// sees that the URL already matches and replaces instead.
const navigateType = state.pushRef.pendingPush ? 'push' : 'replace'
return navigateToKnownRoute(
now,
state,
currentUrl,
currentCanonicalUrl,
refreshSeed,
currentUrl,
currentRenderedSearch,
state.root,
freshnessPolicy,
nextUrlForRefresh,
scrollBehavior,
navigateType,
navigationLock,
// A refresh is bound to the shared map.
segmentCacheMap,
null,
// Refresh navigations don't use route prediction, so there's no route
// cache entry to mark as having a dynamic rewrite on mismatch. If a
// mismatch occurs, the retry handler will traverse the known route tree
// to find and mark the entry.
null,
signal
)Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:43-118
Warning
If a prior navigation has not yet committed its state (state.pushRef.pendingPush), the refresh reducer action takes over the history push operation; otherwise, HistoryUpdater replaces the entry if the URL matches.
Sources: packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts:90-94
Hot Module Replacement (HMR) Refresh Cycles
The hmrRefreshReducer function processes HMR refresh actions, inspecting action.signal to verify whether an incoming HMR generation was canceled or superseded while waiting behind server actions in the router queue. If the signal has aborted, it exits early; otherwise, it delegates to refreshDynamicData using FreshnessPolicy.HMRRefresh.
export function hmrRefreshReducer(
state: ReadonlyReducerState,
action: HmrRefreshAction
): ReducerState {
// HMR actions may wait behind a Server Action in the router queue. If a newer
// generation superseded this one before it started, don't install a refresh
// tree whose request is already canceled.
if (action.signal?.aborted) {
return state
}
return refreshDynamicData(state, FreshnessPolicy.HMRRefresh, action.signal)
}Sources: packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts:9-21
History Traversal and State Restoration
Restoring Previous Router States
When the browser triggers history traversal events such as popstate, restoreReducer processes RestoreAction objects to reconstruct prior router states. It inspects action.historyState to recover the serialized FlightRouterState tree and rendered search parameters. If a history entry lacks this tree—such as entries created before hydration or anchor-link hash navigation—the reducer falls back to the existing state.tree to avoid breaking the router state machine.
export function restoreReducer(
state: ReadonlyReducerState,
action: RestoreAction
): ReducerState {
// This action is used to restore the router state from the history state.
// However, it's possible that the history state no longer contains the `FlightRouterState`.
// We will copy over the internal state on pushState/replaceState events, but if a history entry
// occurred before hydration, or if the user navigated to a hash using a regular anchor link,
// the history state will not contain the `FlightRouterState`.
// In this case, we'll continue to use the existing tree so the router doesn't get into an invalid state.
let treeToRestore: FlightRouterState | undefined
let renderedSearch: string | undefined
const historyState = action.historyState
if (historyState) {
treeToRestore = historyState.tree
renderedSearch = historyState.renderedSearch
} else {
treeToRestore = state.tree
renderedSearch = state.renderedSearch
}Sources: packages/next/src/client/components/router-reducer/reducers/restore-reducer.ts:23-42
Note
restoreReducer calculates the restored next URL using extractPathFromFlightRouterState(treeToRestore), falling back to restoredUrl.pathname if no interception path exists within the restored tree.
Sources: packages/next/src/client/components/router-reducer/reducers/restore-reducer.ts:45-48
Synchronizing Segment Cache and Traversal Seeds
To re-establish traversal state, restoreReducer creates a navigation seed via createNavigationSeed utilizing UnknownDynamicStaleTime and the restored search parameters. It initiates a PPR navigation with FreshnessPolicy.HistoryTraversal, bound directly to the shared segmentCacheMap without any shell restriction.
const restoreSeed = createNavigationSeed(
now,
treeToRestore,
// No transport data (and so no vary params, no partiality, and no
// pathname to parse params from) — this converts the base tree alone.
null,
null,
true,
null,
renderedSearch,
null,
UnknownDynamicStaleTime
)
const navigation = startPPRNavigation(
now,
currentUrl,
state.renderedSearch,
state.root,
restoreSeed.root,
FreshnessPolicy.HistoryTraversal,
restoreSeed.dynamicStaleAt,
false,
accumulation,
// A history-traversal restore is bound to the shared map.
segmentCacheMap,
// A history-traversal restore never restricts to the shell.
false
)Sources: packages/next/src/client/components/router-reducer/reducers/restore-reducer.ts:56-83
Caution
If startPPRNavigation returns null during a history traversal, restoreReducer immediately aborts and falls back to a hard navigation via completeHardNavigation(state, restoredUrl, 'replace').
Sources: packages/next/src/client/components/router-reducer/reducers/restore-reducer.ts:85-87
Spawning Dynamic Requests and Resetting Locks
After establishing the navigation task, spawnDynamicRequests is invoked with a 'replace' navigation type and an un-gated null navigation lock to dispatch requests for missing segments without withholding data behind testing locks. Under testing APIs, resetNavigationLockToPending resets the lock state to a fresh pending scope. Finally, completeTraverseNavigation updates the router state with the new traversal task.
spawnDynamicRequests(
navigation,
restoredUrl,
restoredNextUrl,
FreshnessPolicy.HistoryTraversal,
accumulation,
// History traversal doesn't use route prediction, so there's no route
// cache entry to mark as having a dynamic rewrite on mismatch. If a
// mismatch occurs, the retry handler will traverse the known route tree
// to find and mark the entry.
null,
// History traversal always uses 'replace'.
'replace',
// Instant Navigation Testing API: a traversal is not a capture. Spawn its
// dynamic requests ungated (null lock) so they render from cache or fetch
// normally rather than being withheld behind the lock.
null,
// A history-traversal restore is bound to the shared map.
segmentCacheMap,
// Not an HMR refresh, so there's no request generation to cancel.
undefined
)
// Instant Navigation Testing API: a traversal resets the lock to a fresh
// pending scope — releasing any data withheld by prior forward navigations and
// returning the panel to "awaiting" — without ending the testing session.
// No-op when the testing API is disabled or no lock is held.
resetNavigationLockToPending()
return completeTraverseNavigation(
state,
restoredUrl,
renderedSearch,
navigation,
restoredNextUrl
)Sources: packages/next/src/client/components/router-reducer/reducers/restore-reducer.ts:88-121
Important
lastCommittedTree tracks the flight router state from the last state committed to browser history via HistoryUpdater insertion effects, allowing the server-patch subsystem to distinguish retries of uncommitted transitions.
Sources: packages/next/src/client/components/router-reducer/reducers/committed-state.ts:1-15
Server Patch Application and Fetching
Processing Server Patches and Retries
When a route mismatch or server patch action occurs, serverPatchReducer processes ServerPatchAction objects to handle retries. If action.mpa is true or action.seed is null, the reducer falls back immediately to a hard navigation via completeHardNavigation.
const retryMpa = action.mpa
const retryUrl = new URL(action.url, location.origin)
const retrySeed = action.seed
const navigateType = action.navigateType
if (retryMpa || retrySeed === null) {
// If the server did not send back data during the mismatch, fall back to
// an MPA navigation.
return completeHardNavigation(state, retryUrl, navigateType)
}Sources: packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts:25-33
Warning
If action.previousTree does not match state.tree, a more recent navigation has superseded the mismatch, prompting the reducer to abort the retry and trigger a refreshReducer update to evict stale dynamic data.
Sources: packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts:36-41
Streaming and Decoding Server Responses
fetchServerResponse handles network requests for Flight data, incorporating request headers such as RSC_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, and NEXT_URL. Responses are processed via processFetch, which inspects the leading partiality byte when cache components are enabled.
const headers: RequestHeaders = {
// Enable flight response
[RSC_HEADER]: '1',
// Provide the current router state
[NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(
flightRouterState,
options.isHmrRefresh
),
}
if (process.env.NODE_ENV === 'development' && options.isHmrRefresh) {
headers[NEXT_HMR_REFRESH_HEADER] = '1'
}
if (nextUrl) {
headers[NEXT_URL] = nextUrl
}
Sources: packages/next/src/client/components/router-reducer/fetch-server-response.ts:152-169
Note
When __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS is active, processFetch tees the response body multiple times to provision independent readers for the main Flight decoder, static-stage extractor, and shell-stage extractor.
Sources: packages/next/src/client/components/router-reducer/fetch-server-response.ts:403-414
Related
On This Page
OverviewAction Dispatch and Reducer RoutingDispatch Loop and Reducer RoutingOperation Interface and Sequential Queue CoordinationInitial Router State HydrationSegment Route Trees and Seed DecodingStatic and Runtime Stream CachingNavigation Transitions and Route TreesHandling Client Navigation ActionsStale Time Verification and ConstantsTransitioning Router State Between Route TreesCache Invalidation and Refresh OperationsSegment Caching and Refresh Reducer ExecutionDynamic Data Re-fetching and Navigation SeedsHot Module Replacement (HMR) Refresh CyclesHistory Traversal and State RestorationRestoring Previous Router StatesSynchronizing Segment Cache and Traversal SeedsSpawning Dynamic Requests and Resetting LocksServer Patch Application and FetchingProcessing Server Patches and RetriesStreaming and Decoding Server ResponsesRelated