Skip to content

fix(app-router): fulfill pending segment data when reused during pushState or traversal - #100502

Draft
wasim-builds wants to merge 1 commit into
vercel:canaryfrom
wasim-builds:fix/issue-100489-pushstate-stuck-transition
Draft

wasim-builds wants to merge 1 commit into
vercel:canaryfrom
wasim-builds:fix/issue-100489-pushstate-stuck-transition

Conversation

@wasim-builds

@wasim-builds wasim-builds commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #100489

What?

When a navigation or history.pushState traversal occurs while an earlier action's refresh or navigation is still streaming in (or was superseded), the shared route segments may reuse the existing cache node.

Previously, updateRenderTreeOnNavigation and segment cache reuse paths hardcoded needsDynamicRequest = false without checking if the reused cache node's rsc was still a pending DeferredRsc. As a result, spawnDynamicRequests skipped requesting dynamic data for the unfulfilled segments, leaving React's transition lanes suspended (pendingLanes = suspendedLanes = entangledLanes = 0b111000000000) and useTransition.isPending stuck on true forever until a full page reload.

Why?

finishNavigationTask case NavigationTaskExitStatus.Canceled intentionally leaves pending cache nodes untouched under the contract:

"Its cache nodes may already be reused by the newer navigation, so leave them untouched for the newer request to fulfill."

However, because reusing the existing segment unconditionally set needsDynamicRequest = false, the newer request was never spawned, and nothing fulfilled the pending deferred RSC promises.

How?

  1. In updateRenderTreeOnNavigation, check oldRscDidResolve = !isDeferredRsc(oldRsc) || oldRsc.status !== 'pending' when reusing oldRenderTree.data. If oldRsc has not resolved, set needsDynamicRequest = !oldRscDidResolve so that createDynamicRequestTree and spawnDynamicRequests schedule a request to fulfill the missing segment data.
  2. Applied the same needsDynamicRequest = !oldRscDidResolve check to BFCache and param-unchanged segment reuse paths in render-tree.ts.
  3. Added an e2e regression test in test/e2e/app-dir/concurrent-navigations/server-patch-history.test.ts reproducing the discarded Server Action + in-flight streamed refresh + consecutive pushState calls.

…State or traversal

When a navigation or pushState history traversal occurs while an earlier
navigation/refresh is still streaming or superseded, the shared route
segments may reuse the existing cache node. Previously, reusing the
segment hardcoded `needsDynamicRequest = false` without checking if the
reused node's `rsc` was still pending. This caused `spawnDynamicRequests`
to skip fetching the missing dynamic data, leaving React's transition
lanes suspended and `useTransition.isPending` stuck on `true` forever.

Now, `needsDynamicRequest` is set to `!oldRscDidResolve` when reusing
cache nodes, ensuring any still-pending deferred RSC promise is fulfilled
by a dynamic request.

Fixes vercel#100489.
@wasim-builds wasim-builds changed the title test fix(app-router): fulfill pending segment data when reused during pushState or traversal Oct 21, 2026
searchParams: Promise<Record<string, string>>
}) {
await searchParams
return (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this fixture's cacheComponents: true, awaiting searchParams without an enclosing Suspense boundary fails production prerendering and prevents the concurrent-navigation tests from starting.

Fix on Vercel

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useTransition stays pending forever when history.pushState lands while a discarded Server Action's refresh is still streaming

1 participant