Skip to content

fix: startTransition in popstate should not show Suspense fallback#668

Open
everettbu wants to merge 1 commit into
mainfrom
fix/popstate-suspense-fallback
Open

fix: startTransition in popstate should not show Suspense fallback#668
everettbu wants to merge 1 commit into
mainfrom
fix/popstate-suspense-fallback

Conversation

@everettbu

Copy link
Copy Markdown

Mirror of facebook/react#35975
Original author: fresh3nough


Summary

When startTransition is called inside a popstate event handler and the component suspends inside a <Suspense> boundary, React incorrectly shows the fallback instead of keeping the previous UI visible.

Root Cause

The popstate eager transition path in getNextLanesToFlushSync adds an artificial SyncLane to the render lanes as a priority marker. This causes includesOnlyTransitions() to return false, which breaks two critical checks:

  1. renderDidSuspendDelayIfPossible() does not set workInProgressRootIsPrerendering = true, so renderRootSync does not yield when the shell suspends
  2. finishConcurrentRender() commits the fallback for RootSuspendedWithDelay instead of suspending indefinitely

Fix

Add a flag (workInProgressRootIsEagerPopstateTransition) that tracks when the current render was initiated as a popstate eager transition. This flag is used alongside the existing includesOnlyTransitions() check in:

  • renderDidSuspendDelayIfPossible: enables prerendering mode so renderRootSync yields when the shell suspends
  • finishConcurrentRender: prevents committing the fallback, falling through to RootSuspendedAtTheShell behavior

This ensures the popstate transition falls back to normal async transition behavior when it cannot complete synchronously.

Test Plan

Added a new test case popstate transition with Suspense boundary should not show fallback that verifies:

  • The transition is attempted synchronously during the popstate microtask
  • The Suspense fallback is NOT shown (previous UI remains visible)
  • After the promise resolves, the new UI is rendered

All existing popstate, Suspense, and transition tests continue to pass.

Fixes #35966

When startTransition is called inside a popstate event handler, the
popstate eager transition path adds an artificial SyncLane to the
render lanes. This caused includesOnlyTransitions() to return false,
which prevented the suspension from being handled like a normal
transition - showing the Suspense fallback instead of keeping the
previous UI visible.

Add a flag (workInProgressRootIsEagerPopstateTransition) that tracks
when the current render was initiated as a popstate eager transition.
Use this flag in renderDidSuspendDelayIfPossible and
finishConcurrentRender to treat the render like a transition despite
the artificial SyncLane, falling back to async behavior when the
component suspends.

Fixes: #35966
Signed-off-by: Ubuntu <ubuntu@ip-172-31-31-131.us-east-2.compute.internal>
@greptile-apps

greptile-apps Bot commented Mar 7, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes a bug where startTransition called inside a popstate event handler would incorrectly show the Suspense fallback instead of keeping the previous UI visible when the component suspends.

  • The root cause is that getNextLanesToFlushSync adds an artificial SyncLane to popstate eager transition render lanes, causing includesOnlyTransitions() to return false. This broke two critical code paths: renderDidSuspendDelayIfPossible (which should enable prerendering mode) and finishConcurrentRender (which should suspend indefinitely instead of committing the fallback).
  • The fix introduces a workInProgressRootIsEagerPopstateTransition flag that is captured at the start of each render via prepareFreshStack and used alongside the existing includesOnlyTransitions() check in both affected code paths. The flag has a clean lifecycle — it's set from currentSyncTransitionIsPopstate in the root scheduler (which is scoped to the microtask flush) and re-evaluated on every fresh render.
  • Includes a thorough test case that verifies the previous UI remains visible during suspension, the fallback is pre-warmed but not committed, and the new UI renders correctly after the promise resolves.

Confidence Score: 5/5

  • This PR is safe to merge — it's a well-scoped bug fix with correct flag lifecycle management and a comprehensive test.
  • The changes are minimal and precisely targeted. The two new flags (currentSyncTransitionIsPopstate in the scheduler and workInProgressRootIsEagerPopstateTransition in the work loop) have clean lifecycles: the scheduler flag is set only during the popstate microtask flush and unconditionally reset afterward; the work loop flag is re-evaluated on every fresh render via prepareFreshStack, ensuring it doesn't leak across renders. The fix is additive — it only widens the conditions under which React treats a suspended render as a transition (keeping previous UI), and does not alter any existing code paths for non-popstate transitions. The test thoroughly validates the expected behavior.
  • No files require special attention

Important Files Changed

Filename Overview
packages/react-reconciler/src/ReactFiberRootScheduler.js Adds currentSyncTransitionIsPopstate flag that is set when a popstate eager transition is being flushed, and reset unconditionally after the flush. Clean, well-scoped change.
packages/react-reconciler/src/ReactFiberWorkLoop.js Adds workInProgressRootIsEagerPopstateTransition flag captured in prepareFreshStack, used in renderDidSuspendDelayIfPossible and finishConcurrentRender to treat popstate eager transitions like normal transitions when they suspend. Flag lifecycle is correct — reset on each new render.
packages/react-dom/src/tests/ReactDOMFiberAsync-test.js Adds comprehensive test verifying that startTransition inside a popstate event handler does not show Suspense fallback when the component suspends, matching the behavior of non-popstate transitions.

Last reviewed commit: 7ad96ed

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.

1 participant