From d835a2120c268a3d4d827058069ce65a59d00165 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 4 Nov 2024 13:03:39 -0500 Subject: [PATCH 01/10] Add test for SuspenseList useID bug --- .../src/__tests__/ReactDOMUseId-test.js | 300 +++++++++++++++++- 1 file changed, 298 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index 895c2f9b0dae7..d55bde5cec7a6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -24,6 +24,8 @@ let buffer = ''; let hasErrored = false; let fatalError = undefined; let waitForPaint; +let SuspenseList; +let assertConsoleErrorDev; describe('useId', () => { beforeEach(() => { @@ -32,11 +34,16 @@ describe('useId', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); clientAct = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); Suspense = React.Suspense; useId = React.useId; useState = React.useState; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.unstable_SuspenseList; + } const InternalTestUtils = require('internal-test-utils'); waitForPaint = InternalTestUtils.waitForPaint; @@ -375,6 +382,295 @@ describe('useId', () => { `); }); + it('Supports SuspenseList (reveal order default)', async () => { + function Baz({id, children}) { + return {children}; + } + + function Bar({children}) { + const id = useId(); + return {children}; + } + + function Foo() { + return ( + + A + B + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + + await clientAct(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + }); + + it('Supports SuspenseList (reveal order "together")', async () => { + function Baz({id, children}) { + return {children}; + } + + function Bar({children}) { + const id = useId(); + return {children}; + } + + function Foo() { + return ( + + A + B + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + + await clientAct(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + }); + + it('Supports SuspenseList (reveal order "forwards")', async () => { + function Baz({id, children}) { + return {children}; + } + + function Bar({children}) { + const id = useId(); + return {children}; + } + + function Foo() { + return ( + + A + B + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + + await clientAct(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + + // TODO: this is a bug with useID and SuspenseList revealOrder "forwards" + assertConsoleErrorDev( + [ + `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + +- A server/client branch \`if (typeof window !== 'undefined')\`. +- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. +- Date formatting in a user's locale which doesn't match the server. +- External changing data without sending a snapshot of it along with the HTML. +- Invalid HTML tag nesting. + +It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + +https://react.dev/link/hydration-mismatch + + + + + + ++ A + + + ++ B +`, + ], + {withoutStack: true}, + ); + + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + }); + + it('Supports SuspenseList (reveal order "backwards")', async () => { + function Baz({id, children}) { + return {children}; + } + + function Bar({children}) { + const id = useId(); + return {children}; + } + + function Foo() { + return ( + + A + B + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + + await expect(async () => { + await clientAct(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + // TODO: this is a bug with useID and SuspenseList revealOrder "backwards" + }).rejects.toThrowError( + `Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.`, + ); + + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + }); + it('basic incremental hydration', async () => { function App() { return ( @@ -674,7 +970,7 @@ describe('useId', () => {
_R_0_ - +
_R_7_
@@ -692,7 +988,7 @@ describe('useId', () => {
_R_0_ - +
_R_7_
From 97bd241c9e619ee83b30473bc11d9aa1101f2406 Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 4 Nov 2024 14:15:13 -0500 Subject: [PATCH 02/10] fix whitespace --- packages/react-dom/src/__tests__/ReactDOMUseId-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index d55bde5cec7a6..20aa6d5a08837 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -970,7 +970,7 @@ https://react.dev/link/hydration-mismatch
_R_0_ - +
_R_7_
@@ -988,7 +988,7 @@ https://react.dev/link/hydration-mismatch
_R_0_ - +
_R_7_
From 379815e530d162414eecbd95ba882aeff80fbbce Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 4 Nov 2024 14:16:39 -0500 Subject: [PATCH 03/10] @gate enableSuspenseList --- packages/react-dom/src/__tests__/ReactDOMUseId-test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index 20aa6d5a08837..c22fcebeb120e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -382,6 +382,7 @@ describe('useId', () => { `); }); + // @gate enableSuspenseList it('Supports SuspenseList (reveal order default)', async () => { function Baz({id, children}) { return {children}; @@ -444,6 +445,7 @@ describe('useId', () => { `); }); + // @gate enableSuspenseList it('Supports SuspenseList (reveal order "together")', async () => { function Baz({id, children}) { return {children}; @@ -506,6 +508,7 @@ describe('useId', () => { `); }); + // @gate enableSuspenseList it('Supports SuspenseList (reveal order "forwards")', async () => { function Baz({id, children}) { return {children}; @@ -604,6 +607,7 @@ https://react.dev/link/hydration-mismatch `); }); + // @gate enableSuspenseList it('Supports SuspenseList (reveal order "backwards")', async () => { function Baz({id, children}) { return {children}; From 5b6ba01c5277a5f1e25323de66333c4572389df0 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 4 Nov 2024 14:50:59 -0500 Subject: [PATCH 04/10] Fix tests --- .../src/__tests__/ReactDOMUseId-test.js | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index c22fcebeb120e..6134209df0749 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -648,16 +648,17 @@ https://react.dev/link/hydration-mismatch
`); - await expect(async () => { - await clientAct(async () => { - ReactDOMClient.hydrateRoot(container, ); - }); + if (__DEV__) { // TODO: this is a bug with useID and SuspenseList revealOrder "backwards" - }).rejects.toThrowError( - `Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.`, - ); + await expect(async () => { + await clientAct(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + }).rejects.toThrowError( + `Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.`, + ); - expect(container).toMatchInlineSnapshot(` + expect(container).toMatchInlineSnapshot(`
@@ -673,6 +674,28 @@ https://react.dev/link/hydration-mismatch
`); + } else { + await clientAct(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + } }); it('basic incremental hydration', async () => { From e4e36127aa743ce6472c0283c7b64c7daaddc8e2 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 4 Nov 2024 15:06:49 -0500 Subject: [PATCH 05/10] Fix tests --- .../src/__tests__/ReactDOMUseId-test.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index 6134209df0749..1ac7e3b2a4037 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -7,6 +7,7 @@ * @emails react-core * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ +import {favorSafetyOverHydrationPerf} from 'shared/forks/ReactFeatureFlags.www-dynamic'; let JSDOM; let React; @@ -648,7 +649,7 @@ https://react.dev/link/hydration-mismatch
`); - if (__DEV__) { + if (gate(flags => flags.favorSafetyOverHydrationPerf)) { // TODO: this is a bug with useID and SuspenseList revealOrder "backwards" await expect(async () => { await clientAct(async () => { @@ -679,6 +680,34 @@ https://react.dev/link/hydration-mismatch ReactDOMClient.hydrateRoot(container, ); }); + // TODO: this seems like a bug when `favorSafetyOverHydrationPerf` is false? + assertConsoleErrorDev( + [ + `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + +- A server/client branch \`if (typeof window !== 'undefined')\`. +- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. +- Date formatting in a user's locale which doesn't match the server. +- External changing data without sending a snapshot of it along with the HTML. +- Invalid HTML tag nesting. + +It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + +https://react.dev/link/hydration-mismatch + + + + + + + ++ B +- A +`, + ], + {withoutStack: true}, + ); + expect(container).toMatchInlineSnapshot(`
Date: Mon, 4 Nov 2024 15:13:59 -0500 Subject: [PATCH 06/10] Fix lint --- packages/react-dom/src/__tests__/ReactDOMUseId-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index 1ac7e3b2a4037..f18c204476f15 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -7,8 +7,6 @@ * @emails react-core * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ -import {favorSafetyOverHydrationPerf} from 'shared/forks/ReactFeatureFlags.www-dynamic'; - let JSDOM; let React; let ReactDOMClient; From 38be7720df57697ba2c3e2eb4e8a40c41bbddc06 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 9 Jun 2025 17:11:47 -0400 Subject: [PATCH 07/10] Update separator character --- .../src/__tests__/ReactDOMUseId-test.js | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index f18c204476f15..5cc37f2cf1876 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -410,12 +410,12 @@ describe('useId', () => { id="container" > A B @@ -431,12 +431,12 @@ describe('useId', () => { id="container" > A B @@ -473,12 +473,12 @@ describe('useId', () => { id="container" > A B @@ -494,12 +494,12 @@ describe('useId', () => { id="container" > A B @@ -536,12 +536,12 @@ describe('useId', () => { id="container" > A B @@ -570,17 +570,17 @@ https://react.dev/link/hydration-mismatch - + + A - + + B `, @@ -593,12 +593,12 @@ https://react.dev/link/hydration-mismatch id="container" > A B @@ -635,12 +635,12 @@ https://react.dev/link/hydration-mismatch id="container" > A B @@ -662,12 +662,12 @@ https://react.dev/link/hydration-mismatch id="container" > A B @@ -697,8 +697,8 @@ https://react.dev/link/hydration-mismatch - - + + + B - A `, @@ -711,12 +711,12 @@ https://react.dev/link/hydration-mismatch id="container" > A B From bb8613ef79f4796d33424c16139fd272e161cae1 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 9 Jun 2025 17:14:54 -0400 Subject: [PATCH 08/10] Specify explicit options --- .../src/__tests__/ReactDOMUseId-test.js | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index 5cc37f2cf1876..b37b6af38e5f7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -382,7 +382,7 @@ describe('useId', () => { }); // @gate enableSuspenseList - it('Supports SuspenseList (reveal order default)', async () => { + it('Supports SuspenseList (reveal order independent)', async () => { function Baz({id, children}) { return {children}; } @@ -394,7 +394,7 @@ describe('useId', () => { function Foo() { return ( - + A B @@ -520,7 +520,7 @@ describe('useId', () => { function Foo() { return ( - + A B @@ -553,9 +553,8 @@ describe('useId', () => { }); // TODO: this is a bug with useID and SuspenseList revealOrder "forwards" - assertConsoleErrorDev( - [ - `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + assertConsoleErrorDev([ + `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: - A server/client branch \`if (typeof window !== 'undefined')\`. - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. @@ -568,7 +567,7 @@ It can also happen if the client has a browser extension installed which messes https://react.dev/link/hydration-mismatch - + + B `, - ], - {withoutStack: true}, - ); + ]); expect(container).toMatchInlineSnapshot(`
+ A B @@ -654,7 +651,7 @@ https://react.dev/link/hydration-mismatch ReactDOMClient.hydrateRoot(container, ); }); }).rejects.toThrowError( - `Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.`, + `Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client.`, ); expect(container).toMatchInlineSnapshot(` @@ -679,9 +676,8 @@ https://react.dev/link/hydration-mismatch }); // TODO: this seems like a bug when `favorSafetyOverHydrationPerf` is false? - assertConsoleErrorDev( - [ - `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + assertConsoleErrorDev([ + `A tree hydrated but some attributes of the server rendered text didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: - A server/client branch \`if (typeof window !== 'undefined')\`. - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. @@ -694,7 +690,7 @@ It can also happen if the client has a browser extension installed which messes https://react.dev/link/hydration-mismatch - + @@ -702,9 +698,7 @@ https://react.dev/link/hydration-mismatch + B - A `, - ], - {withoutStack: true}, - ); + ]); expect(container).toMatchInlineSnapshot(`
Date: Mon, 9 Jun 2025 17:24:27 -0400 Subject: [PATCH 09/10] Keep track of how many forks we had after reconcilinng --- packages/react-reconciler/src/ReactFiberBeginWork.js | 8 ++++++++ .../react-reconciler/src/ReactFiberSuspenseComponent.js | 2 ++ 2 files changed, 10 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index a9316163156f5..1dfc37c648ca5 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -3342,6 +3342,7 @@ function initSuspenseListRenderState( tail: null | Fiber, lastContentRow: null | Fiber, tailMode: SuspenseListTailMode, + treeForkCount: number, ): void { const renderState: null | SuspenseListRenderState = workInProgress.memoizedState; @@ -3353,6 +3354,7 @@ function initSuspenseListRenderState( last: lastContentRow, tail: tail, tailMode: tailMode, + treeForkCount: treeForkCount, }: SuspenseListRenderState); } else { // We can reuse the existing object from previous renders. @@ -3362,6 +3364,7 @@ function initSuspenseListRenderState( renderState.last = lastContentRow; renderState.tail = tail; renderState.tailMode = tailMode; + renderState.treeForkCount = treeForkCount; } } @@ -3404,6 +3407,8 @@ function updateSuspenseListComponent( validateSuspenseListChildren(newChildren, revealOrder); reconcileChildren(current, workInProgress, newChildren, renderLanes); + // Read how many children forks this set pushed so we can push it every time we retry. + const treeForkCount = getForksAtLevel(workInProgress); if (!shouldForceFallback) { const didSuspendBefore = @@ -3446,6 +3451,7 @@ function updateSuspenseListComponent( tail, lastContentRow, tailMode, + treeForkCount, ); break; } @@ -3478,6 +3484,7 @@ function updateSuspenseListComponent( tail, null, // last tailMode, + treeForkCount, ); break; } @@ -3488,6 +3495,7 @@ function updateSuspenseListComponent( null, // tail null, // last undefined, + treeForkCount, ); break; } diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index 2542d660c4c47..64ab4f29fcd14 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -54,6 +54,8 @@ export type SuspenseListRenderState = { tail: null | Fiber, // Tail insertions setting. tailMode: SuspenseListTailMode, + // Keep track of total number of forks during multiple passes + treeForkCount: number, }; export type RetryQueue = Set; From 5edb07d0ad1b311554a65497acad9b72adccbdf5 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 9 Jun 2025 17:29:15 -0400 Subject: [PATCH 10/10] Reapply the total child count when re-attempting each step --- .../src/__tests__/ReactDOMUseId-test.js | 100 +++++++++++------- .../src/ReactFiberBeginWork.js | 2 +- .../src/ReactFiberCompleteWork.js | 10 +- 3 files changed, 73 insertions(+), 39 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index b37b6af38e5f7..0654e8c6e6851 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -552,53 +552,76 @@ describe('useId', () => { ReactDOMClient.hydrateRoot(container, ); }); - // TODO: this is a bug with useID and SuspenseList revealOrder "forwards" - assertConsoleErrorDev([ - `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: - -- A server/client branch \`if (typeof window !== 'undefined')\`. -- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. -- Date formatting in a user's locale which doesn't match the server. -- External changing data without sending a snapshot of it along with the HTML. -- Invalid HTML tag nesting. + expect(container).toMatchInlineSnapshot(` +
+ + A + + + B + +
+ `); + }); -It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + // @gate enableSuspenseList + it('Supports SuspenseList (reveal order "backwards") with a single child in a list of many', async () => { + function Baz({id, children}) { + return {children}; + } -https://react.dev/link/hydration-mismatch + function Bar({children}) { + const id = useId(); + return {children}; + } - - - - - -+ A - - - -+ B -`, - ]); + function Foo() { + return ( + + {null} + A + {null} + + ); + } + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); expect(container).toMatchInlineSnapshot(`
A + +
+ `); + + await clientAct(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + + expect(container).toMatchInlineSnapshot(` +
- B + A +
`); }); @@ -645,7 +668,7 @@ https://react.dev/link/hydration-mismatch `); if (gate(flags => flags.favorSafetyOverHydrationPerf)) { - // TODO: this is a bug with useID and SuspenseList revealOrder "backwards" + // TODO: This is a bug with revealOrder="backwards" in that it hydrates in reverse. await expect(async () => { await clientAct(async () => { ReactDOMClient.hydrateRoot(container, ); @@ -675,9 +698,9 @@ https://react.dev/link/hydration-mismatch ReactDOMClient.hydrateRoot(container, ); }); - // TODO: this seems like a bug when `favorSafetyOverHydrationPerf` is false? + // TODO: This is a bug with revealOrder="backwards" in that it hydrates in reverse. assertConsoleErrorDev([ - `A tree hydrated but some attributes of the server rendered text didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: - A server/client branch \`if (typeof window !== 'undefined')\`. - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. @@ -690,11 +713,14 @@ It can also happen if the client has a browser extension installed which messes https://react.dev/link/hydration-mismatch - + - - + + + B - A `, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 1dfc37c648ca5..2b9e4e7fdb9b0 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -3408,7 +3408,7 @@ function updateSuspenseListComponent( reconcileChildren(current, workInProgress, newChildren, renderLanes); // Read how many children forks this set pushed so we can push it every time we retry. - const treeForkCount = getForksAtLevel(workInProgress); + const treeForkCount = getIsHydrating() ? getForksAtLevel(workInProgress) : 0; if (!shouldForceFallback) { const didSuspendBefore = diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index d62e3c3fea11d..a415e6217ab8a 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -184,7 +184,7 @@ import {resetChildFibers} from './ReactChildFiber'; import {createScopeInstance} from './ReactFiberScope'; import {transferActualDuration} from './ReactProfilerTimer'; import {popCacheProvider} from './ReactFiberCacheComponent'; -import {popTreeContext} from './ReactFiberTreeContext'; +import {popTreeContext, pushTreeFork} from './ReactFiberTreeContext'; import {popRootTransition, popTransition} from './ReactFiberTransition'; import { popMarkerInstance, @@ -1764,6 +1764,10 @@ function completeWork( ForceSuspenseFallback, ), ); + if (getIsHydrating()) { + // Re-apply tree fork since we popped the tree fork context in the beginning of this function. + pushTreeFork(workInProgress, renderState.treeForkCount); + } // Don't bubble properties in this case. return workInProgress.child; } @@ -1890,6 +1894,10 @@ function completeWork( } pushSuspenseListContext(workInProgress, suspenseContext); // Do a pass over the next row. + if (getIsHydrating()) { + // Re-apply tree fork since we popped the tree fork context in the beginning of this function. + pushTreeFork(workInProgress, renderState.treeForkCount); + } // Don't bubble properties in this case. return next; }