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
salazarm committed
Nov 18, 2021 at 08:16 UTC
fdc1d617a44cb9015c3bac3a6341b5af3311faf6
14 files changed
+94
-23
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+54
-16
@@ -1680,19 +1680,25 @@ describe('ReactDOMFizzServer', () => {
1680
1681
// @gate experimental
1682
it('calls getServerSnapshot instead of getSnapshot', async () => {
1683
+ const ref = React.createRef();
1684
+
1685
function getServerSnapshot() {
1686
return 'server';
1687
}
1688
+
1689
function getClientSnapshot() {
1690
return 'client';
1691
}
1692
+
1693
function subscribe() {
1694
return () => {};
1695
}
1696
+
1697
function Child({text}) {
1698
Scheduler.unstable_yieldValue(text);
1699
return text;
1700
}
1701
+
1702
function App() {
1703
const value = useSyncExternalStore(
1704
subscribe,
@@ -1700,11 +1706,12 @@ describe('ReactDOMFizzServer', () => {
1706
getServerSnapshot,
1707
);
1708
return (
1703
- <div>
1709
+ <div ref={ref}>
1710
<Child text={value} />
1711
</div>
1712
);
1713
}
1714
+
1715
const loggedErrors = [];
1716
await act(async () => {
1717
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -1723,14 +1730,29 @@ describe('ReactDOMFizzServer', () => {
1730
1731
ReactDOM.hydrateRoot(container, <App />);
1732
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
1752
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
+ }
1756
});
1757
1758
// The selector implementation uses the lazy ref initialization pattern
@@ -1790,15 +1812,31 @@ describe('ReactDOMFizzServer', () => {
1812
1813
ReactDOM.hydrateRoot(container, <App />);
1814
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
1836
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
+ }
1840
});
1841
1842
// @gate experimental
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+11
-1
@@ -199,7 +199,16 @@ describe('ReactDOMServerPartialHydration', () => {
199
// hydrating anyway.
200
suspend = true;
201
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
+ }
212
jest.runAllTimers();
213
214
// Expect the server-generated HTML to stay intact.
@@ -215,6 +224,7 @@ describe('ReactDOMServerPartialHydration', () => {
224
expect(container.textContent).toBe('HelloHello');
225
});
226
227
+ // @gate enableClientRenderFallbackOnHydrationMismatch
228
it('falls back to client rendering boundary on mismatch', async () => {
229
let client = false;
230
let suspend = false;
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+9
-3
@@ -62,7 +62,10 @@ import {
62
didNotFindHydratableTextInstance,
63
didNotFindHydratableSuspenseInstance,
64
} from './ReactFiberHostConfig';
65
-import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
65
+import {
66
+ enableClientRenderFallbackOnHydrationMismatch,
67
+ enableSuspenseServerRenderer,
68
+} from 'shared/ReactFeatureFlags';
69
import {OffscreenLane} from './ReactFiberLane.new';
70
import {
71
getSuspendedTreeContext,
@@ -324,8 +327,11 @@ function tryHydrate(fiber, nextInstance) {
327
}
328
}
329
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
+ ) {
335
throw new Error(
336
'An error occurred during hydration. The server HTML was replaced with client content',
337
);
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+9
-3
@@ -62,7 +62,10 @@ import {
62
didNotFindHydratableTextInstance,
63
didNotFindHydratableSuspenseInstance,
64
} from './ReactFiberHostConfig';
65
-import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
65
+import {
66
+ enableClientRenderFallbackOnHydrationMismatch,
67
+ enableSuspenseServerRenderer,
68
+} from 'shared/ReactFeatureFlags';
69
import {OffscreenLane} from './ReactFiberLane.old';
70
import {
71
getSuspendedTreeContext,
@@ -324,8 +327,11 @@ function tryHydrate(fiber, nextInstance) {
327
}
328
}
329
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
+ ) {
335
throw new Error(
336
'An error occurred during hydration. The server HTML was replaced with client content',
337
);
packages/shared/ReactFeatureFlags.js
+2
@@ -105,6 +105,8 @@ export const enableSuspenseAvoidThisFallback = false;
105
106
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
107
108
+export const enableClientRenderFallbackOnHydrationMismatch = true;
109
+
110
export const enableComponentStackLocations = true;
111
112
export const enableNewReconciler = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -51,6 +51,7 @@ export const warnAboutSpreadingKeyToJSX = false;
51
export const warnOnSubscriptionInsideStartTransition = false;
52
export const enableSuspenseAvoidThisFallback = false;
53
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
54
+export const enableClientRenderFallbackOnHydrationMismatch = true;
55
export const enableComponentStackLocations = false;
56
export const enableLegacyFBSupport = false;
57
export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
42
export const warnOnSubscriptionInsideStartTransition = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45
+export const enableClientRenderFallbackOnHydrationMismatch = true;
46
export const enableComponentStackLocations = false;
47
export const enableLegacyFBSupport = false;
48
export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
42
export const warnOnSubscriptionInsideStartTransition = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45
+export const enableClientRenderFallbackOnHydrationMismatch = true;
46
export const enableComponentStackLocations = true;
47
export const enableLegacyFBSupport = false;
48
export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -52,6 +52,7 @@ export const deferRenderPhaseUpdateToNextBatch = false;
52
export const warnOnSubscriptionInsideStartTransition = false;
53
export const enableSuspenseAvoidThisFallback = false;
54
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
55
+export const enableClientRenderFallbackOnHydrationMismatch = true;
56
export const enableStrictEffects = false;
57
export const createRootStrictEffectsByDefault = false;
58
export const enableUseRefAccessWarning = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
42
export const warnOnSubscriptionInsideStartTransition = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45
+export const enableClientRenderFallbackOnHydrationMismatch = true;
46
export const enableComponentStackLocations = true;
47
export const enableLegacyFBSupport = false;
48
export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
42
export const warnOnSubscriptionInsideStartTransition = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45
+export const enableClientRenderFallbackOnHydrationMismatch = true;
46
export const enableComponentStackLocations = true;
47
export const enableLegacyFBSupport = false;
48
export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
42
export const warnOnSubscriptionInsideStartTransition = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45
+export const enableClientRenderFallbackOnHydrationMismatch = true;
46
export const enableComponentStackLocations = true;
47
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
48
export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -28,6 +28,7 @@ export const enableSyncDefaultUpdates = __VARIANT__;
28
export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
29
export const warnOnSubscriptionInsideStartTransition = __VARIANT__;
30
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
31
+export const enableClientRenderFallbackOnHydrationMismatch = __VARIANT__;
32
33
// Enable this flag to help with concurrent mode debugging.
34
// It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -33,6 +33,7 @@ export const {
33
enableSyncDefaultUpdates,
34
warnOnSubscriptionInsideStartTransition,
35
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
36
+ enableClientRenderFallbackOnHydrationMismatch,
37
} = dynamicFeatureFlags;
38
39
// On WWW, __EXPERIMENTAL__ is used for a new modern build.