@samitouri / QOS-React-2 / commits / fdc1d617a4

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.