[Fizz] Don't discard a queued dehydrated boundary on a concurrent update#37016
Open
Saransh-Jainbu wants to merge 1 commit into
Open
[Fizz] Don't discard a queued dehydrated boundary on a concurrent update#37016Saransh-Jainbu wants to merge 1 commit into
Saransh-Jainbu wants to merge 1 commit into
Conversation
|
Comparing: 7023f50...383569b Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: Expand to show |
When a concurrent client update (didReceiveUpdate) reaches a dehydrated Suspense boundary, updateDehydratedSuspenseComponent fell through to retrySuspenseComponentWithoutHydrating for both the still-pending ($?) and already-queued-for-reveal ($~) states. For the $~ case the completed content has already streamed in and been queued for a throttled/rAF reveal. Discarding it to client-render orphans that streamed segment and races the in-flight reveal, so both the client-rendered tree and the still-unrevealed streamed segment coexist in the DOM until the queued reveal runs and cleans up. This is the transient DOM duplication reported in react#37014 (path 2: a concurrent app-level update racing hydration, as opposed to the document-load timing of path 1). Split the $~ case off first and give it the same treatment the existing "still pending" branch already uses: keep the dehydrated child, mark DidCapture | Callback, and bail. The Callback flag re-registers the _reactRetry callback that the Fizz reveal invokes, so hydration resumes cleanly once the reveal swaps the content into place. The $? path is unchanged. Adds isSuspenseInstanceQueued to the host config (DOM config, both reconciler forks, and the no-hydration shims) and a regression test in ReactDOMFizzServer that reproduces the race with independent server/client promises and requestAnimationFrame interception. The test fails without the reconciler change and passes with it.
Saransh-Jainbu
force-pushed
the
fizz-discard-queued-dehydrated-boundary
branch
from
July 15, 2026 06:18
95c630d to
383569b
Compare
eps1lon
self-requested a review
July 15, 2026 09:46
daltino
approved these changes
Jul 15, 2026
daltino
left a comment
There was a problem hiding this comment.
This PR introduces a fix to ensure a queued dehydrated boundary is not discarded during concurrent updates, addressing a race condition in Fizz hydration. The changes are well-focused, adding a new isSuspenseInstanceQueued utility and incorporating it into the reconciliation logic. The tests provide ample coverage for the edge case, adhering to the contribution guidelines.
Contributor
Author
|
@eps1lon could you take a look when you get a chance? Quick summary of what this fixes and what I verified:
Thanks in advance! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes path 2 of #37014 — the transient DOM duplication where streamed Suspense content briefly exists twice.
When a concurrent client update (
didReceiveUpdate) reaches a dehydrated Suspense boundary,updateDehydratedSuspenseComponentfell through toretrySuspenseComponentWithoutHydratingfor both the still-pending ($?) and already-queued-for-reveal ($~) states.For the
$~case the completed content has already streamed in and been queued for a throttled/rAF reveal (React 19.2 batched reveals). Discarding it to client-render orphans that streamed segment and races the in-flight reveal, so the client-rendered tree and the still-unrevealed streamed segment coexist in the DOM until the queued reveal runs and cleans up — exactly the ~300ms duplication window the issue describes.This is the second trigger path called out in the issue thread (a concurrent app-level update racing hydration), distinct from the document-load timing of path 1. It corresponds to the long-standing TODO in
updateDehydratedSuspenseComponent:Fix
Split the
$~case off first and give it the same treatment the existing "still pending" branch already uses: keep the dehydrated child, markDidCapture | Callback, and bail. TheCallbackflag re-registers the_reactRetrycallback that the Fizz reveal invokes, so hydration resumes cleanly once the reveal swaps the content into place. The$?path is unchanged.Supporting plumbing: a new
isSuspenseInstanceQueuedhost-config method, threaded through the DOM config, both reconciler forks (custom/noop), and both no-hydration shims.Behavior (verified by instrumenting the DOM)
$~marker gone — boundary discarded, streamed<span>orphaned$~intact, streamed content preservedTest
Adds a regression test in
ReactDOMFizzServer-test.jsthat reproduces the race with:requestAnimationFrameinterception — holds the queued reveal captive so the concurrent update lands while the boundary is genuinely$~.The test fails without the reconciler change and passes with it.
Caveat
This is a rigorous React-only reproduction of the mechanism. I was not able to reproduce the reporter's exact Next.js App Router end-to-end scenario locally (that needs the Next runtime), but the reconciler-level race and its fix are demonstrated deterministically.
Test plan
yarn test ReactDOMFizzServer— 168/168 pass (new test included)yarn test ReactDOMServerPartialHydration— 65/65 pass (no regression to$?behavior)yarn lincclean,yarn prettierapplied, Flow reports no errors