Skip to content

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

Open
fresh3nough wants to merge 1 commit intofacebook:mainfrom
fresh3nough:fix/popstate-suspense-fallback
Open

fix: startTransition in popstate should not show Suspense fallback#35975
fresh3nough wants to merge 1 commit intofacebook:mainfrom
fresh3nough:fix/popstate-suspense-fallback

Conversation

@fresh3nough
Copy link
Contributor

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: facebook#35966
Signed-off-by: Ubuntu <[email protected]>
@meta-cla meta-cla bot added the CLA Signed label Mar 7, 2026
@react-sizebot
Copy link

Comparing: 4610359...7ad96ed

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.07% 611.79 kB 612.20 kB +0.06% 108.12 kB 108.18 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.07% 677.72 kB 678.17 kB +0.06% 119.08 kB 119.15 kB
facebook-www/ReactDOM-prod.classic.js +0.06% 697.67 kB 698.12 kB +0.05% 122.58 kB 122.64 kB
facebook-www/ReactDOM-prod.modern.js +0.06% 687.98 kB 688.43 kB +0.05% 120.96 kB 121.02 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 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.

Bug: startTransition inside popstate shows Suspense fallback instead of previous UI

2 participants