Skip to content

Commit fdc1d61

Browse files
authored
Flag for client render fallback behavior on hydration mismatch (#22787)
* Add flag for new client-render fallback behavior on hydration mismatch * gate test * gate tests too * fix test gating
1 parent f320ef8 commit fdc1d61

14 files changed

+94
-23
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,31 +1680,38 @@ describe('ReactDOMFizzServer', () => {
16801680

16811681
// @gate experimental
16821682
it('calls getServerSnapshot instead of getSnapshot', async () => {
1683+
const ref = React.createRef();
1684+
16831685
function getServerSnapshot() {
16841686
return 'server';
16851687
}
1688+
16861689
function getClientSnapshot() {
16871690
return 'client';
16881691
}
1692+
16891693
function subscribe() {
16901694
return () => {};
16911695
}
1696+
16921697
function Child({text}) {
16931698
Scheduler.unstable_yieldValue(text);
16941699
return text;
16951700
}
1701+
16961702
function App() {
16971703
const value = useSyncExternalStore(
16981704
subscribe,
16991705
getClientSnapshot,
17001706
getServerSnapshot,
17011707
);
17021708
return (
1703-
<div>
1709+
<div ref={ref}>
17041710
<Child text={value} />
17051711
</div>
17061712
);
17071713
}
1714+
17081715
const loggedErrors = [];
17091716
await act(async () => {
17101717
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -1723,14 +1730,29 @@ describe('ReactDOMFizzServer', () => {
17231730

17241731
ReactDOM.hydrateRoot(container, <App />);
17251732

1726-
expect(() => {
1727-
// The first paint switches to client rendering due to mismatch
1733+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
1734+
expect(() => {
1735+
// The first paint switches to client rendering due to mismatch
1736+
expect(Scheduler).toFlushUntilNextPaint(['client']);
1737+
}).toErrorDev(
1738+
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1739+
{withoutStack: true},
1740+
);
1741+
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1742+
} else {
1743+
const serverRenderedDiv = container.getElementsByTagName('div')[0];
1744+
// The first paint uses the server snapshot
1745+
expect(Scheduler).toFlushUntilNextPaint(['server']);
1746+
expect(getVisibleChildren(container)).toEqual(<div>server</div>);
1747+
// Hydration succeeded
1748+
expect(ref.current).toEqual(serverRenderedDiv);
1749+
1750+
// Asynchronously we detect that the store has changed on the client,
1751+
// and patch up the inconsistency
17281752
expect(Scheduler).toFlushUntilNextPaint(['client']);
1729-
}).toErrorDev(
1730-
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1731-
{withoutStack: true},
1732-
);
1733-
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1753+
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1754+
expect(ref.current).toEqual(serverRenderedDiv);
1755+
}
17341756
});
17351757

17361758
// The selector implementation uses the lazy ref initialization pattern
@@ -1790,15 +1812,31 @@ describe('ReactDOMFizzServer', () => {
17901812

17911813
ReactDOM.hydrateRoot(container, <App />);
17921814

1793-
// The first paint uses the client due to mismatch forcing client render
1794-
expect(() => {
1795-
// The first paint switches to client rendering due to mismatch
1815+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
1816+
// The first paint uses the client due to mismatch forcing client render
1817+
expect(() => {
1818+
// The first paint switches to client rendering due to mismatch
1819+
expect(Scheduler).toFlushUntilNextPaint(['client']);
1820+
}).toErrorDev(
1821+
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1822+
{withoutStack: true},
1823+
);
1824+
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1825+
} else {
1826+
const serverRenderedDiv = container.getElementsByTagName('div')[0];
1827+
1828+
// The first paint uses the server snapshot
1829+
expect(Scheduler).toFlushUntilNextPaint(['server']);
1830+
expect(getVisibleChildren(container)).toEqual(<div>server</div>);
1831+
// Hydration succeeded
1832+
expect(ref.current).toEqual(serverRenderedDiv);
1833+
1834+
// Asynchronously we detect that the store has changed on the client,
1835+
// and patch up the inconsistency
17961836
expect(Scheduler).toFlushUntilNextPaint(['client']);
1797-
}).toErrorDev(
1798-
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1799-
{withoutStack: true},
1800-
);
1801-
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1837+
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1838+
expect(ref.current).toEqual(serverRenderedDiv);
1839+
}
18021840
});
18031841

18041842
// @gate experimental

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,16 @@ describe('ReactDOMServerPartialHydration', () => {
199199
// hydrating anyway.
200200
suspend = true;
201201
ReactDOM.hydrateRoot(container, <App />);
202-
Scheduler.unstable_flushAll();
202+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
203+
Scheduler.unstable_flushAll();
204+
} else {
205+
expect(() => {
206+
Scheduler.unstable_flushAll();
207+
}).toErrorDev(
208+
// TODO: This error should not be logged in this case. It's a false positive.
209+
'Did not expect server HTML to contain the text node "Hello" in <div>.',
210+
);
211+
}
203212
jest.runAllTimers();
204213

205214
// Expect the server-generated HTML to stay intact.
@@ -215,6 +224,7 @@ describe('ReactDOMServerPartialHydration', () => {
215224
expect(container.textContent).toBe('HelloHello');
216225
});
217226

227+
// @gate enableClientRenderFallbackOnHydrationMismatch
218228
it('falls back to client rendering boundary on mismatch', async () => {
219229
let client = false;
220230
let suspend = false;

packages/react-reconciler/src/ReactFiberHydrationContext.new.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ import {
6262
didNotFindHydratableTextInstance,
6363
didNotFindHydratableSuspenseInstance,
6464
} from './ReactFiberHostConfig';
65-
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
65+
import {
66+
enableClientRenderFallbackOnHydrationMismatch,
67+
enableSuspenseServerRenderer,
68+
} from 'shared/ReactFeatureFlags';
6669
import {OffscreenLane} from './ReactFiberLane.new';
6770
import {
6871
getSuspendedTreeContext,
@@ -324,8 +327,11 @@ function tryHydrate(fiber, nextInstance) {
324327
}
325328
}
326329

327-
function throwOnHydrationMismatchIfConcurrentMode(fiber) {
328-
if ((fiber.mode & ConcurrentMode) !== NoMode) {
330+
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
331+
if (
332+
enableClientRenderFallbackOnHydrationMismatch &&
333+
(fiber.mode & ConcurrentMode) !== NoMode
334+
) {
329335
throw new Error(
330336
'An error occurred during hydration. The server HTML was replaced with client content',
331337
);

packages/react-reconciler/src/ReactFiberHydrationContext.old.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ import {
6262
didNotFindHydratableTextInstance,
6363
didNotFindHydratableSuspenseInstance,
6464
} from './ReactFiberHostConfig';
65-
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
65+
import {
66+
enableClientRenderFallbackOnHydrationMismatch,
67+
enableSuspenseServerRenderer,
68+
} from 'shared/ReactFeatureFlags';
6669
import {OffscreenLane} from './ReactFiberLane.old';
6770
import {
6871
getSuspendedTreeContext,
@@ -324,8 +327,11 @@ function tryHydrate(fiber, nextInstance) {
324327
}
325328
}
326329

327-
function throwOnHydrationMismatchIfConcurrentMode(fiber) {
328-
if ((fiber.mode & ConcurrentMode) !== NoMode) {
330+
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
331+
if (
332+
enableClientRenderFallbackOnHydrationMismatch &&
333+
(fiber.mode & ConcurrentMode) !== NoMode
334+
) {
329335
throw new Error(
330336
'An error occurred during hydration. The server HTML was replaced with client content',
331337
);

packages/shared/ReactFeatureFlags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export const enableSuspenseAvoidThisFallback = false;
105105

106106
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
107107

108+
export const enableClientRenderFallbackOnHydrationMismatch = true;
109+
108110
export const enableComponentStackLocations = true;
109111

110112
export const enableNewReconciler = false;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const warnAboutSpreadingKeyToJSX = false;
5151
export const warnOnSubscriptionInsideStartTransition = false;
5252
export const enableSuspenseAvoidThisFallback = false;
5353
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
54+
export const enableClientRenderFallbackOnHydrationMismatch = true;
5455
export const enableComponentStackLocations = false;
5556
export const enableLegacyFBSupport = false;
5657
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
4242
export const warnOnSubscriptionInsideStartTransition = false;
4343
export const enableSuspenseAvoidThisFallback = false;
4444
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45+
export const enableClientRenderFallbackOnHydrationMismatch = true;
4546
export const enableComponentStackLocations = false;
4647
export const enableLegacyFBSupport = false;
4748
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
4242
export const warnOnSubscriptionInsideStartTransition = false;
4343
export const enableSuspenseAvoidThisFallback = false;
4444
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45+
export const enableClientRenderFallbackOnHydrationMismatch = true;
4546
export const enableComponentStackLocations = true;
4647
export const enableLegacyFBSupport = false;
4748
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.native.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const deferRenderPhaseUpdateToNextBatch = false;
5252
export const warnOnSubscriptionInsideStartTransition = false;
5353
export const enableSuspenseAvoidThisFallback = false;
5454
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
55+
export const enableClientRenderFallbackOnHydrationMismatch = true;
5556
export const enableStrictEffects = false;
5657
export const createRootStrictEffectsByDefault = false;
5758
export const enableUseRefAccessWarning = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
4242
export const warnOnSubscriptionInsideStartTransition = false;
4343
export const enableSuspenseAvoidThisFallback = false;
4444
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45+
export const enableClientRenderFallbackOnHydrationMismatch = true;
4546
export const enableComponentStackLocations = true;
4647
export const enableLegacyFBSupport = false;
4748
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.testing.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
4242
export const warnOnSubscriptionInsideStartTransition = false;
4343
export const enableSuspenseAvoidThisFallback = false;
4444
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45+
export const enableClientRenderFallbackOnHydrationMismatch = true;
4546
export const enableComponentStackLocations = true;
4647
export const enableLegacyFBSupport = false;
4748
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.testing.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
4242
export const warnOnSubscriptionInsideStartTransition = false;
4343
export const enableSuspenseAvoidThisFallback = false;
4444
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45+
export const enableClientRenderFallbackOnHydrationMismatch = true;
4546
export const enableComponentStackLocations = true;
4647
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
4748
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.www-dynamic.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const enableSyncDefaultUpdates = __VARIANT__;
2828
export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
2929
export const warnOnSubscriptionInsideStartTransition = __VARIANT__;
3030
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
31+
export const enableClientRenderFallbackOnHydrationMismatch = __VARIANT__;
3132

3233
// Enable this flag to help with concurrent mode debugging.
3334
// It logs information to the console about React scheduling, rendering, and commit phases.

packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const {
3333
enableSyncDefaultUpdates,
3434
warnOnSubscriptionInsideStartTransition,
3535
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
36+
enableClientRenderFallbackOnHydrationMismatch,
3637
} = dynamicFeatureFlags;
3738

3839
// On WWW, __EXPERIMENTAL__ is used for a new modern build.

0 commit comments

Comments
 (0)