@samitouri / QOS-React-2 / commits / 142d4f1c00

useMutableSource hydration support (#18771)

* useMutableSource hydration support * Remove unnecessary ReactMutableSource fork * Replaced root.registerMutableSourceForHydration() with mutableSources option * Response to PR feedback: 1. Moved mutableSources root option to hydrationOptions object 2. Only initialize root mutableSourceEagerHydrationData if supportsHydration config is true 3. Lazily initialize mutableSourceEagerHydrationData on root object

Brian Vaughn committed May 21, 2020 at 16:00 UTC 142d4f1c00c66f3d728177082dbc027fd6335115
15 files changed +626 -87
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+30 -68
@@ -1445,8 +1445,6 @@ describe('ReactDOMServerHooks', () => {
1445 .getAttribute('id');
1446 expect(serverId).not.toBeNull();
1447
1448 - const childOneSpan = container.getElementsByTagName('span')[0];
1449 -
1448 const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1449 root.render(<App show={false} />);
1450 expect(Scheduler).toHaveYielded([]);
@@ -1462,25 +1460,15 @@ describe('ReactDOMServerHooks', () => {
1460 // State update should trigger the ID to update, which changes the props
1461 // of ChildWithID. This should cause ChildWithID to hydrate before Children
1462
1465 - expect(Scheduler).toFlushAndYieldThrough(
1466 - __DEV__
1467 - ? [
1468 - 'Child with ID',
1469 - // Fallbacks are immediately committed in TestUtils version
1470 - // of act
1471 - // 'Child with ID',
1472 - // 'Child with ID',
1473 - 'Child One',
1474 - 'Child Two',
1475 - ]
1476 - : [
1477 - 'Child with ID',
1478 - 'Child with ID',
1479 - 'Child with ID',
1480 - 'Child One',
1481 - 'Child Two',
1482 - ],
1483 - );
1463 + expect(Scheduler).toFlushAndYieldThrough([
1464 + 'Child with ID',
1465 + // Fallbacks are immediately committed in TestUtils version
1466 + // of act
1467 + // 'Child with ID',
1468 + // 'Child with ID',
1469 + 'Child One',
1470 + 'Child Two',
1471 + ]);
1472
1473 expect(child1Ref.current).toBe(null);
1474 expect(childWithIDRef.current).toEqual(
@@ -1500,7 +1488,9 @@ describe('ReactDOMServerHooks', () => {
1488 });
1489
1490 // Children hydrates after ChildWithID
1503 - expect(child1Ref.current).toBe(childOneSpan);
1491 + expect(child1Ref.current).toBe(
1492 + container.getElementsByTagName('span')[0],
1493 + );
1494
1495 Scheduler.unstable_flushAll();
1496
@@ -1606,9 +1596,7 @@ describe('ReactDOMServerHooks', () => {
1596 ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
1597 <App />,
1598 );
1609 - expect(() =>
1610 - expect(() => Scheduler.unstable_flushAll()).toThrow(),
1611 - ).toErrorDev([
1599 + expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1600 'Warning: Expected server HTML to contain a matching <div> in <div>.',
1601 ]);
1602 });
@@ -1694,14 +1682,12 @@ describe('ReactDOMServerHooks', () => {
1682 ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
1683 <App />,
1684 );
1697 - expect(() =>
1698 - expect(() => Scheduler.unstable_flushAll()).toThrow(),
1699 - ).toErrorDev([
1685 + expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1686 'Warning: Expected server HTML to contain a matching <div> in <div>.',
1687 ]);
1688 });
1689
1704 - it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1690 + it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
1691 function Child({appId}) {
1692 return <div aria-labelledby={appId + ''} />;
1693 }
@@ -1718,12 +1704,7 @@ describe('ReactDOMServerHooks', () => {
1704 ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
1705 <App />,
1706 );
1721 - expect(() =>
1722 - expect(() => Scheduler.unstable_flushAll()).toThrow(
1723 - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1724 - 'Do not read the value directly.',
1725 - ),
1726 - ).toErrorDev(
1707 + expect(() => Scheduler.unstable_flushAll()).toErrorDev(
1708 [
1709 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1710 'Warning: Did not expect server HTML to contain a <span> in <div>.',
@@ -1732,7 +1713,7 @@ describe('ReactDOMServerHooks', () => {
1713 );
1714 });
1715
1735 - it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1716 + it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
1717 function Child({appId}) {
1718 return <div aria-labelledby={appId + ''} />;
1719 }
@@ -1749,12 +1730,7 @@ describe('ReactDOMServerHooks', () => {
1730 ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
1731 <App />,
1732 );
1752 - expect(() =>
1753 - expect(() => Scheduler.unstable_flushAll()).toThrow(
1754 - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1755 - 'Do not read the value directly.',
1756 - ),
1757 - ).toErrorDev(
1733 + expect(() => Scheduler.unstable_flushAll()).toErrorDev(
1734 [
1735 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1736 'Warning: Did not expect server HTML to contain a <span> in <div>.',
@@ -1763,7 +1739,7 @@ describe('ReactDOMServerHooks', () => {
1739 );
1740 });
1741
1766 - it('useOpaqueIdentifier throws if you try to use the result as a string in a child component', async () => {
1742 + it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => {
1743 function Child({appId}) {
1744 return <div aria-labelledby={appId + ''} />;
1745 }
@@ -1779,12 +1755,7 @@ describe('ReactDOMServerHooks', () => {
1755 ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
1756 <App />,
1757 );
1782 - expect(() =>
1783 - expect(() => Scheduler.unstable_flushAll()).toThrow(
1784 - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1785 - 'Do not read the value directly.',
1786 - ),
1787 - ).toErrorDev(
1758 + expect(() => Scheduler.unstable_flushAll()).toErrorDev(
1759 [
1760 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1761 'Warning: Did not expect server HTML to contain a <div> in <div>.',
@@ -1793,7 +1764,7 @@ describe('ReactDOMServerHooks', () => {
1764 );
1765 });
1766
1796 - it('useOpaqueIdentifier throws if you try to use the result as a string', async () => {
1767 + it('useOpaqueIdentifier warns if you try to use the result as a string', async () => {
1768 function App() {
1769 const id = useOpaqueIdentifier();
1770 return <div aria-labelledby={id + ''} />;
@@ -1806,12 +1777,7 @@ describe('ReactDOMServerHooks', () => {
1777 ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
1778 <App />,
1779 );
1809 - expect(() =>
1810 - expect(() => Scheduler.unstable_flushAll()).toThrow(
1811 - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1812 - 'Do not read the value directly.',
1813 - ),
1814 - ).toErrorDev(
1780 + expect(() => Scheduler.unstable_flushAll()).toErrorDev(
1781 [
1782 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1783 'Warning: Did not expect server HTML to contain a <div> in <div>.',
@@ -1820,7 +1786,7 @@ describe('ReactDOMServerHooks', () => {
1786 );
1787 });
1788
1823 - it('useOpaqueIdentifier throws if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
1789 + it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
1790 function Child({appId}) {
1791 return <div aria-labelledby={appId + ''} />;
1792 }
@@ -1842,16 +1808,14 @@ describe('ReactDOMServerHooks', () => {
1808 <App />,
1809 );
1810
1845 - if (
1846 - gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
1847 - ) {
1811 + if (gate(flags => !flags.new)) {
1812 expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1813 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1814 'Do not read the value directly.',
1815 ]);
1816 } else {
1853 - // In the old reconciler, the error isn't surfaced to the user. That
1854 - // part isn't important, as long as It warns.
1817 + // This error isn't surfaced to the user; only the warning is.
1818 + // The error is just the mechanism that restarts the render.
1819 expect(() =>
1820 expect(() => Scheduler.unstable_flushAll()).toThrow(
1821 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
@@ -1864,7 +1828,7 @@ describe('ReactDOMServerHooks', () => {
1828 }
1829 });
1830
1867 - it('useOpaqueIdentifier throws if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
1831 + it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
1832 function Child({appId}) {
1833 return <div aria-labelledby={+appId} />;
1834 }
@@ -1888,16 +1852,14 @@ describe('ReactDOMServerHooks', () => {
1852 <App />,
1853 );
1854
1891 - if (
1892 - gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
1893 - ) {
1855 + if (gate(flags => !flags.new)) {
1856 expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1857 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1858 'Do not read the value directly.',
1859 ]);
1860 } else {
1899 - // In the old reconciler, the error isn't surfaced to the user. That
1900 - // part isn't important, as long as It warns.
1861 + // This error isn't surfaced to the user; only the warning is.
1862 + // The error is just the mechanism that restarts the render.
1863 expect(() =>
1864 expect(() => Scheduler.unstable_flushAll()).toThrow(
1865 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
packages/react-dom/src/client/ReactDOMRoot.js
+17 -2
@@ -9,9 +9,8 @@
9
10 import type {Container} from './ReactDOMHostConfig';
11 import type {RootTag} from 'react-reconciler/src/ReactRootTags';
12 -import type {ReactNodeList} from 'shared/ReactTypes';
12 +import type {MutableSource, ReactNodeList} from 'shared/ReactTypes';
13 import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
14 -import {findHostInstanceWithNoPortals} from 'react-reconciler/src/ReactFiberReconciler';
14
15 export type RootType = {
16 render(children: ReactNodeList): void,
@@ -25,6 +24,7 @@ export type RootOptions = {
24 hydrationOptions?: {
25 onHydrated?: (suspenseNode: Comment) => void,
26 onDeleted?: (suspenseNode: Comment) => void,
27 + mutableSources?: Array<MutableSource<any>>,
28 ...
29 },
30 ...
@@ -47,6 +47,8 @@ import {ensureListeningTo} from './ReactDOMComponent';
47 import {
48 createContainer,
49 updateContainer,
50 + findHostInstanceWithNoPortals,
51 + registerMutableSourceForHydration,
52 } from 'react-reconciler/src/ReactFiberReconciler';
53 import invariant from 'shared/invariant';
54 import {
@@ -124,6 +126,11 @@ function createRootImpl(
126 const hydrate = options != null && options.hydrate === true;
127 const hydrationCallbacks =
128 (options != null && options.hydrationOptions) || null;
129 + const mutableSources =
130 + (options != null &&
131 + options.hydrationOptions != null &&
132 + options.hydrationOptions.mutableSources) ||
133 + null;
134 const root = createContainer(container, tag, hydrate, hydrationCallbacks);
135 markContainerAsRoot(root.current, container);
136 const containerNodeType = container.nodeType;
@@ -143,6 +150,14 @@ function createRootImpl(
150 ) {
151 ensureListeningTo(container, 'onMouseEnter');
152 }
153 +
154 + if (mutableSources) {
155 + for (let i = 0; i < mutableSources.length; i++) {
156 + const mutableSource = mutableSources[i];
157 + registerMutableSourceForHydration(root, mutableSource);
158 + }
159 + }
160 +
161 return root;
162 }
163
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+28
@@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
13 import type {Fiber} from './ReactInternalTypes';
14 import type {FiberRoot} from './ReactInternalTypes';
15 import type {Lanes, Lane} from './ReactFiberLane';
16 +import type {MutableSource} from 'shared/ReactTypes';
17 import type {
18 SuspenseState,
19 SuspenseListRenderState,
@@ -126,6 +127,7 @@ import {
127 isSuspenseInstancePending,
128 isSuspenseInstanceFallback,
129 registerSuspenseInstanceRetry,
130 + supportsHydration,
131 } from './ReactFiberHostConfig';
132 import type {SuspenseInstance} from './ReactFiberHostConfig';
133 import {shouldSuspend} from './ReactFiberReconciler';
@@ -193,8 +195,12 @@ import {
195 markSkippedUpdateLanes,
196 getWorkInProgressRoot,
197 pushRenderLanes,
198 + getExecutionContext,
199 + RetryAfterError,
200 + NoContext,
201 } from './ReactFiberWorkLoop.new';
202 import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
203 +import {setWorkInProgressVersion} from './ReactMutableSource.new';
204
205 import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
206
@@ -1074,6 +1080,20 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1080 // be any children to hydrate which is effectively the same thing as
1081 // not hydrating.
1082
1083 + if (supportsHydration) {
1084 + const mutableSourceEagerHydrationData =
1085 + root.mutableSourceEagerHydrationData;
1086 + if (mutableSourceEagerHydrationData != null) {
1087 + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1088 + const mutableSource = ((mutableSourceEagerHydrationData[
1089 + i
1090 + ]: any): MutableSource<any>);
1091 + const version = mutableSourceEagerHydrationData[i + 1];
1092 + setWorkInProgressVersion(mutableSource, version);
1093 + }
1094 + }
1095 + }
1096 +
1097 const child = mountChildFibers(
1098 workInProgress,
1099 null,
@@ -2256,6 +2276,14 @@ function updateDehydratedSuspenseComponent(
2276 // but after we've already committed once.
2277 warnIfHydrating();
2278
2279 + if ((getExecutionContext() & RetryAfterError) !== NoContext) {
2280 + return retrySuspenseComponentWithoutHydrating(
2281 + current,
2282 + workInProgress,
2283 + renderLanes,
2284 + );
2285 + }
2286 +
2287 if ((workInProgress.mode & BlockingMode) === NoMode) {
2288 return retrySuspenseComponentWithoutHydrating(
2289 current,
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+28
@@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
13 import type {Fiber} from './ReactInternalTypes';
14 import type {FiberRoot} from './ReactInternalTypes';
15 import type {ExpirationTime} from './ReactFiberExpirationTime.old';
16 +import type {MutableSource} from 'shared/ReactTypes';
17 import type {
18 SuspenseState,
19 SuspenseListRenderState,
@@ -114,6 +115,7 @@ import {
115 isSuspenseInstancePending,
116 isSuspenseInstanceFallback,
117 registerSuspenseInstanceRetry,
118 + supportsHydration,
119 } from './ReactFiberHostConfig';
120 import type {SuspenseInstance} from './ReactFiberHostConfig';
121 import {shouldSuspend} from './ReactFiberReconciler';
@@ -179,8 +181,12 @@ import {
181 renderDidSuspendDelayIfPossible,
182 markUnprocessedUpdateTime,
183 getWorkInProgressRoot,
184 + getExecutionContext,
185 + RetryAfterError,
186 + NoContext,
187 } from './ReactFiberWorkLoop.old';
188 import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
189 +import {setWorkInProgressVersion} from './ReactMutableSource.old';
190
191 import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
192
@@ -1038,6 +1044,20 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
1044 // be any children to hydrate which is effectively the same thing as
1045 // not hydrating.
1046
1047 + if (supportsHydration) {
1048 + const mutableSourceEagerHydrationData =
1049 + root.mutableSourceEagerHydrationData;
1050 + if (mutableSourceEagerHydrationData != null) {
1051 + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1052 + const mutableSource = ((mutableSourceEagerHydrationData[
1053 + i
1054 + ]: any): MutableSource<any>);
1055 + const version = mutableSourceEagerHydrationData[i + 1];
1056 + setWorkInProgressVersion(mutableSource, version);
1057 + }
1058 + }
1059 + }
1060 +
1061 const child = mountChildFibers(
1062 workInProgress,
1063 null,
@@ -2239,6 +2259,14 @@ function updateDehydratedSuspenseComponent(
2259 // but after we've already committed once.
2260 warnIfHydrating();
2261
2262 + if ((getExecutionContext() & RetryAfterError) !== NoContext) {
2263 + return retrySuspenseComponentWithoutHydrating(
2264 + current,
2265 + workInProgress,
2266 + renderExpirationTime,
2267 + );
2268 + }
2269 +
2270 if ((workInProgress.mode & BlockingMode) === NoMode) {
2271 return retrySuspenseComponentWithoutHydrating(
2272 current,
packages/react-reconciler/src/ReactFiberReconciler.js
+6
@@ -49,6 +49,7 @@ import {
49 findBoundingRects as findBoundingRects_old,
50 focusWithin as focusWithin_old,
51 observeVisibleRects as observeVisibleRects_old,
52 + registerMutableSourceForHydration as registerMutableSourceForHydration_old,
53 } from './ReactFiberReconciler.old';
54
55 import {
@@ -86,6 +87,7 @@ import {
87 findBoundingRects as findBoundingRects_new,
88 focusWithin as focusWithin_new,
89 observeVisibleRects as observeVisibleRects_new,
90 + registerMutableSourceForHydration as registerMutableSourceForHydration_new,
91 } from './ReactFiberReconciler.new';
92
93 export const createContainer = enableNewReconciler
@@ -186,3 +188,7 @@ export const focusWithin = enableNewReconciler
188 export const observeVisibleRects = enableNewReconciler
189 ? observeVisibleRects_new
190 : observeVisibleRects_old;
191 +
192 +export const registerMutableSourceForHydration = enableNewReconciler
193 + ? registerMutableSourceForHydration_new
194 + : registerMutableSourceForHydration_old;
packages/react-reconciler/src/ReactFiberReconciler.new.js
+1
@@ -88,6 +88,7 @@ import {
88 findHostInstancesForRefresh,
89 } from './ReactFiberHotReloading.new';
90
91 +export {registerMutableSourceForHydration} from './ReactMutableSource.new';
92 export {createPortal} from './ReactPortal';
93 export {
94 createComponentSelector,
packages/react-reconciler/src/ReactFiberReconciler.old.js
+1
@@ -85,6 +85,7 @@ import {
85 findHostInstancesForRefresh,
86 } from './ReactFiberHotReloading.old';
87
88 +export {registerMutableSourceForHydration} from './ReactMutableSource.old';
89 export {createPortal} from './ReactPortal';
90 export {
91 createComponentSelector,
packages/react-reconciler/src/ReactFiberRoot.new.js
+5 -2
@@ -10,7 +10,7 @@
10 import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
11 import type {RootTag} from './ReactRootTags';
12
13 -import {noTimeout} from './ReactFiberHostConfig';
13 +import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
14 import {createHostRootFiber} from './ReactFiber.new';
15 import {
16 NoLanes,
@@ -47,12 +47,15 @@ function FiberRootNode(containerInfo, tag, hydrate) {
47 this.pingedLanes = NoLanes;
48 this.expiredLanes = NoLanes;
49 this.mutableReadLanes = NoLanes;
50 -
50 this.finishedLanes = NoLanes;
51
52 this.entangledLanes = NoLanes;
53 this.entanglements = createLaneMap(NoLanes);
54
55 + if (supportsHydration) {
56 + this.mutableSourceEagerHydrationData = null;
57 + }
58 +
59 if (enableSchedulerTracing) {
60 this.interactionThreadID = unstable_getThreadID();
61 this.memoizedInteractions = new Set();
packages/react-reconciler/src/ReactFiberRoot.old.js
+5 -1
@@ -11,7 +11,7 @@ import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
11 import type {ExpirationTime} from './ReactFiberExpirationTime.old';
12 import type {RootTag} from './ReactRootTags';
13
14 -import {noTimeout} from './ReactFiberHostConfig';
14 +import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
15 import {createHostRootFiber} from './ReactFiber.old';
16 import {NoWork} from './ReactFiberExpirationTime.old';
17 import {
@@ -47,6 +47,10 @@ function FiberRootNode(containerInfo, tag, hydrate) {
47 this.lastExpiredTime = NoWork;
48 this.mutableSourceLastPendingUpdateTime = NoWork;
49
50 + if (supportsHydration) {
51 + this.mutableSourceEagerHydrationData = null;
52 + }
53 +
54 if (enableSchedulerTracing) {
55 this.interactionThreadID = unstable_getThreadID();
56 this.memoizedInteractions = new Set();
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+31 -7
@@ -62,6 +62,7 @@ import {
62 warnsIfNotActing,
63 beforeActiveInstanceBlur,
64 afterActiveInstanceBlur,
65 + clearContainer,
66 } from './ReactFiberHostConfig';
67
68 import {
@@ -215,13 +216,14 @@ const {
216
217 type ExecutionContext = number;
218
218 -const NoContext = /* */ 0b000000;
219 -const BatchedContext = /* */ 0b000001;
220 -const EventContext = /* */ 0b000010;
221 -const DiscreteEventContext = /* */ 0b000100;
222 -const LegacyUnbatchedContext = /* */ 0b001000;
223 -const RenderContext = /* */ 0b010000;
224 -const CommitContext = /* */ 0b100000;
219 +export const NoContext = /* */ 0b0000000;
220 +const BatchedContext = /* */ 0b0000001;
221 +const EventContext = /* */ 0b0000010;
222 +const DiscreteEventContext = /* */ 0b0000100;
223 +const LegacyUnbatchedContext = /* */ 0b0001000;
224 +const RenderContext = /* */ 0b0010000;
225 +const CommitContext = /* */ 0b0100000;
226 +export const RetryAfterError = /* */ 0b1000000;
227
228 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
229 const RootIncomplete = 0;
@@ -724,6 +726,15 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
726 prepareFreshStack(root, NoLanes);
727 } else if (exitStatus !== RootIncomplete) {
728 if (exitStatus === RootErrored) {
729 + executionContext |= RetryAfterError;
730 +
731 + // If an error occurred during hydration,
732 + // discard server response and fall back to client side render.
733 + if (root.hydrate) {
734 + root.hydrate = false;
735 + clearContainer(root.containerInfo);
736 + }
737 +
738 // If something threw an error, try rendering one more time. We'll render
739 // synchronously to block concurrent data mutations, and we'll includes
740 // all pending updates are included. If it still fails after the second
@@ -976,6 +987,15 @@ function performSyncWorkOnRoot(root) {
987 }
988
989 if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
990 + executionContext |= RetryAfterError;
991 +
992 + // If an error occurred during hydration,
993 + // discard server response and fall back to client side render.
994 + if (root.hydrate) {
995 + root.hydrate = false;
996 + clearContainer(root.containerInfo);
997 + }
998 +
999 // If something threw an error, try rendering one more time. We'll render
1000 // synchronously to block concurrent data mutations, and we'll includes
1001 // all pending updates are included. If it still fails after the second
@@ -1016,6 +1036,10 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
1036 }
1037 }
1038
1039 +export function getExecutionContext(): ExecutionContext {
1040 + return executionContext;
1041 +}
1042 +
1043 export function flushDiscreteUpdates() {
1044 // TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
1045 // However, `act` uses `batchedUpdates`, so there's no way to distinguish
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+31 -7
@@ -74,6 +74,7 @@ import {
74 warnsIfNotActing,
75 beforeActiveInstanceBlur,
76 afterActiveInstanceBlur,
77 + clearContainer,
78 } from './ReactFiberHostConfig';
79
80 import {
@@ -207,13 +208,14 @@ const {
208
209 type ExecutionContext = number;
210
210 -const NoContext = /* */ 0b000000;
211 -const BatchedContext = /* */ 0b000001;
212 -const EventContext = /* */ 0b000010;
213 -const DiscreteEventContext = /* */ 0b000100;
214 -const LegacyUnbatchedContext = /* */ 0b001000;
215 -const RenderContext = /* */ 0b010000;
216 -const CommitContext = /* */ 0b100000;
211 +export const NoContext = /* */ 0b0000000;
212 +const BatchedContext = /* */ 0b0000001;
213 +const EventContext = /* */ 0b0000010;
214 +const DiscreteEventContext = /* */ 0b0000100;
215 +const LegacyUnbatchedContext = /* */ 0b0001000;
216 +const RenderContext = /* */ 0b0010000;
217 +const CommitContext = /* */ 0b0100000;
218 +export const RetryAfterError = /* */ 0b1000000;
219
220 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
221 const RootIncomplete = 0;
@@ -728,6 +730,15 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
730
731 if (exitStatus !== RootIncomplete) {
732 if (exitStatus === RootErrored) {
733 + executionContext |= RetryAfterError;
734 +
735 + // If an error occurred during hydration,
736 + // discard server response and fall back to client side render.
737 + if (root.hydrate) {
738 + root.hydrate = false;
739 + clearContainer(root.containerInfo);
740 + }
741 +
742 // If something threw an error, try rendering one more time. We'll
743 // render synchronously to block concurrent data mutations, and we'll
744 // render at Idle (or lower) so that all pending updates are included.
@@ -1011,6 +1022,15 @@ function performSyncWorkOnRoot(root) {
1022 let exitStatus = renderRootSync(root, expirationTime);
1023
1024 if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
1025 + executionContext |= RetryAfterError;
1026 +
1027 + // If an error occurred during hydration,
1028 + // discard server response and fall back to client side render.
1029 + if (root.hydrate) {
1030 + root.hydrate = false;
1031 + clearContainer(root.containerInfo);
1032 + }
1033 +
1034 // If something threw an error, try rendering one more time. We'll
1035 // render synchronously to block concurrent data mutations, and we'll
1036 // render at Idle (or lower) so that all pending updates are included.
@@ -1051,6 +1071,10 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1071 }
1072 }
1073
1074 +export function getExecutionContext(): ExecutionContext {
1075 + return executionContext;
1076 +}
1077 +
1078 export function flushDiscreteUpdates() {
1079 // TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
1080 // However, `act` uses `batchedUpdates`, so there's no way to distinguish
packages/react-reconciler/src/ReactInternalTypes.js
+6
@@ -16,6 +16,7 @@ import type {
16 ReactContext,
17 MutableSourceSubscribeFn,
18 MutableSourceGetSnapshotFn,
19 + MutableSourceVersion,
20 MutableSource,
21 } from 'shared/ReactTypes';
22 import type {SuspenseInstance} from './ReactFiberHostConfig';
@@ -247,6 +248,11 @@ type BaseFiberRootProperties = {|
248 // when external, mutable sources are read from during render.
249 mutableSourceLastPendingUpdateTime: ExpirationTime,
250
251 + // Used by useMutableSource hook to avoid tearing during hydrtaion.
252 + mutableSourceEagerHydrationData?: Array<
253 + MutableSource<any> | MutableSourceVersion,
254 + > | null,
255 +
256 // Only used by new reconciler
257
258 // Represents the next task that the root should work on, or the current one
packages/react-reconciler/src/ReactMutableSource.new.js
+21
@@ -8,6 +8,7 @@
8 */
9
10 import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes';
11 +import type {FiberRoot} from './ReactInternalTypes';
12
13 import {isPrimaryRenderer} from './ReactFiberHostConfig';
14
@@ -85,3 +86,23 @@ export function warnAboutMultipleRenderersDEV(
86 }
87 }
88 }
89 +
90 +// Eager reads the version of a mutable source and stores it on the root.
91 +// This ensures that the version used for server rendering matches the one
92 +// that is eventually read during hydration.
93 +// If they don't match there's a potential tear and a full deopt render is required.
94 +export function registerMutableSourceForHydration(
95 + root: FiberRoot,
96 + mutableSource: MutableSource<any>,
97 +): void {
98 + const getVersion = mutableSource._getVersion;
99 + const version = getVersion(mutableSource._source);
100 +
101 + // TODO Clear this data once all pending hydration work is finished.
102 + // Retaining it forever may interfere with GC.
103 + if (root.mutableSourceEagerHydrationData == null) {
104 + root.mutableSourceEagerHydrationData = [mutableSource, version];
105 + } else {
106 + root.mutableSourceEagerHydrationData.push(mutableSource, version);
107 + }
108 +}
packages/react-reconciler/src/ReactMutableSource.old.js
+20
@@ -116,3 +116,23 @@ export function warnAboutMultipleRenderersDEV(
116 }
117 }
118 }
119 +
120 +// Eager reads the version of a mutable source and stores it on the root.
121 +// This ensures that the version used for server rendering matches the one
122 +// that is eventually read during hydration.
123 +// If they don't match there's a potential tear and a full deopt render is required.
124 +export function registerMutableSourceForHydration(
125 + root: FiberRoot,
126 + mutableSource: MutableSource<any>,
127 +): void {
128 + const getVersion = mutableSource._getVersion;
129 + const version = getVersion(mutableSource._source);
130 +
131 + // TODO Clear this data once all pending hydration work is finished.
132 + // Retaining it forever may interfere with GC.
133 + if (root.mutableSourceEagerHydrationData == null) {
134 + root.mutableSourceEagerHydrationData = [mutableSource, version];
135 + } else {
136 + root.mutableSourceEagerHydrationData.push(mutableSource, version);
137 + }
138 +}
packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js new
+396
@@ -0,0 +1,396 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactDOM;
14 +let ReactDOMServer;
15 +let Scheduler;
16 +let act;
17 +let useMutableSource;
18 +
19 +describe('useMutableSourceHydration', () => {
20 + beforeEach(() => {
21 + jest.resetModules();
22 +
23 + React = require('react');
24 + ReactDOM = require('react-dom');
25 + ReactDOMServer = require('react-dom/server');
26 + Scheduler = require('scheduler');
27 +
28 + useMutableSource = React.useMutableSource;
29 + act = require('react-dom/test-utils').act;
30 + });
31 +
32 + const defaultGetSnapshot = source => source.value;
33 + const defaultSubscribe = (source, callback) => source.subscribe(callback);
34 +
35 + function createComplexSource(initialValueA, initialValueB) {
36 + const callbacksA = [];
37 + const callbacksB = [];
38 + let revision = 0;
39 + let valueA = initialValueA;
40 + let valueB = initialValueB;
41 +
42 + const subscribeHelper = (callbacks, callback) => {
43 + if (callbacks.indexOf(callback) < 0) {
44 + callbacks.push(callback);
45 + }
46 + return () => {
47 + const index = callbacks.indexOf(callback);
48 + if (index >= 0) {
49 + callbacks.splice(index, 1);
50 + }
51 + };
52 + };
53 +
54 + return {
55 + subscribeA(callback) {
56 + return subscribeHelper(callbacksA, callback);
57 + },
58 + subscribeB(callback) {
59 + return subscribeHelper(callbacksB, callback);
60 + },
61 +
62 + get listenerCountA() {
63 + return callbacksA.length;
64 + },
65 + get listenerCountB() {
66 + return callbacksB.length;
67 + },
68 +
69 + set valueA(newValue) {
70 + revision++;
71 + valueA = newValue;
72 + callbacksA.forEach(callback => callback());
73 + },
74 + get valueA() {
75 + return valueA;
76 + },
77 +
78 + set valueB(newValue) {
79 + revision++;
80 + valueB = newValue;
81 + callbacksB.forEach(callback => callback());
82 + },
83 + get valueB() {
84 + return valueB;
85 + },
86 +
87 + get version() {
88 + return revision;
89 + },
90 + };
91 + }
92 +
93 + function createSource(initialValue) {
94 + const callbacks = [];
95 + let revision = 0;
96 + let value = initialValue;
97 + return {
98 + subscribe(callback) {
99 + if (callbacks.indexOf(callback) < 0) {
100 + callbacks.push(callback);
101 + }
102 + return () => {
103 + const index = callbacks.indexOf(callback);
104 + if (index >= 0) {
105 + callbacks.splice(index, 1);
106 + }
107 + };
108 + },
109 + get listenerCount() {
110 + return callbacks.length;
111 + },
112 + set value(newValue) {
113 + revision++;
114 + value = newValue;
115 + callbacks.forEach(callback => callback());
116 + },
117 + get value() {
118 + return value;
119 + },
120 + get version() {
121 + return revision;
122 + },
123 + };
124 + }
125 +
126 + function createMutableSource(source) {
127 + return React.createMutableSource(source, param => param.version);
128 + }
129 +
130 + function Component({getSnapshot, label, mutableSource, subscribe}) {
131 + const snapshot = useMutableSource(mutableSource, getSnapshot, subscribe);
132 + Scheduler.unstable_yieldValue(`${label}:${snapshot}`);
133 + return <div>{`${label}:${snapshot}`}</div>;
134 + }
135 +
136 + // @gate experimental
137 + it('should render and hydrate', () => {
138 + const source = createSource('one');
139 + const mutableSource = createMutableSource(source);
140 +
141 + function TestComponent() {
142 + return (
143 + <Component
144 + label="only"
145 + getSnapshot={defaultGetSnapshot}
146 + mutableSource={mutableSource}
147 + subscribe={defaultSubscribe}
148 + />
149 + );
150 + }
151 +
152 + const container = document.createElement('div');
153 + document.body.appendChild(container);
154 +
155 + const htmlString = ReactDOMServer.renderToString(<TestComponent />);
156 + container.innerHTML = htmlString;
157 + expect(Scheduler).toHaveYielded(['only:one']);
158 + expect(source.listenerCount).toBe(0);
159 +
160 + const root = ReactDOM.unstable_createRoot(container, {
161 + hydrate: true,
162 + hydrationOptions: {
163 + mutableSources: [mutableSource],
164 + },
165 + });
166 + act(() => {
167 + root.render(<TestComponent />);
168 + });
169 + expect(Scheduler).toHaveYielded(['only:one']);
170 + expect(source.listenerCount).toBe(1);
171 + });
172 +
173 + // @gate experimental
174 + it('should detect a tear before hydrating a component', () => {
175 + const source = createSource('one');
176 + const mutableSource = createMutableSource(source);
177 +
178 + function TestComponent() {
179 + return (
180 + <Component
181 + label="only"
182 + getSnapshot={defaultGetSnapshot}
183 + mutableSource={mutableSource}
184 + subscribe={defaultSubscribe}
185 + />
186 + );
187 + }
188 +
189 + const container = document.createElement('div');
190 + document.body.appendChild(container);
191 +
192 + const htmlString = ReactDOMServer.renderToString(<TestComponent />);
193 + container.innerHTML = htmlString;
194 + expect(Scheduler).toHaveYielded(['only:one']);
195 + expect(source.listenerCount).toBe(0);
196 +
197 + const root = ReactDOM.unstable_createRoot(container, {
198 + hydrate: true,
199 + hydrationOptions: {
200 + mutableSources: [mutableSource],
201 + },
202 + });
203 + expect(() => {
204 + act(() => {
205 + root.render(<TestComponent />);
206 +
207 + source.value = 'two';
208 + });
209 + }).toErrorDev(
210 + 'Warning: Did not expect server HTML to contain a <div> in <div>.',
211 + {withoutStack: true},
212 + );
213 + expect(Scheduler).toHaveYielded(['only:two']);
214 + expect(source.listenerCount).toBe(1);
215 + });
216 +
217 + // @gate experimental
218 + it('should detect a tear between hydrating components', () => {
219 + const source = createSource('one');
220 + const mutableSource = createMutableSource(source);
221 +
222 + function TestComponent() {
223 + return (
224 + <>
225 + <Component
226 + label="a"
227 + getSnapshot={defaultGetSnapshot}
228 + mutableSource={mutableSource}
229 + subscribe={defaultSubscribe}
230 + />
231 + <Component
232 + label="b"
233 + getSnapshot={defaultGetSnapshot}
234 + mutableSource={mutableSource}
235 + subscribe={defaultSubscribe}
236 + />
237 + </>
238 + );
239 + }
240 +
241 + const container = document.createElement('div');
242 + document.body.appendChild(container);
243 +
244 + const htmlString = ReactDOMServer.renderToString(<TestComponent />);
245 + container.innerHTML = htmlString;
246 + expect(Scheduler).toHaveYielded(['a:one', 'b:one']);
247 + expect(source.listenerCount).toBe(0);
248 +
249 + const root = ReactDOM.unstable_createRoot(container, {
250 + hydrate: true,
251 + hydrationOptions: {
252 + mutableSources: [mutableSource],
253 + },
254 + });
255 + expect(() => {
256 + act(() => {
257 + root.render(<TestComponent />);
258 + expect(Scheduler).toFlushAndYieldThrough(['a:one']);
259 + source.value = 'two';
260 + });
261 + }).toErrorDev(
262 + 'Warning: Did not expect server HTML to contain a <div> in <div>.',
263 + {withoutStack: true},
264 + );
265 + expect(Scheduler).toHaveYielded(['a:two', 'b:two']);
266 + expect(source.listenerCount).toBe(2);
267 + });
268 +
269 + // @gate experimental
270 + it('should detect a tear between hydrating components reading from different parts of a source', () => {
271 + const source = createComplexSource('a:one', 'b:one');
272 + const mutableSource = createMutableSource(source);
273 +
274 + // Subscribe to part of the store.
275 + const getSnapshotA = s => s.valueA;
276 + const subscribeA = (s, callback) => s.subscribeA(callback);
277 + const getSnapshotB = s => s.valueB;
278 + const subscribeB = (s, callback) => s.subscribeB(callback);
279 +
280 + const container = document.createElement('div');
281 + document.body.appendChild(container);
282 +
283 + const htmlString = ReactDOMServer.renderToString(
284 + <>
285 + <Component
286 + label="0"
287 + getSnapshot={getSnapshotA}
288 + mutableSource={mutableSource}
289 + subscribe={subscribeA}
290 + />
291 + <Component
292 + label="1"
293 + getSnapshot={getSnapshotB}
294 + mutableSource={mutableSource}
295 + subscribe={subscribeB}
296 + />
297 + </>,
298 + );
299 + container.innerHTML = htmlString;
300 + expect(Scheduler).toHaveYielded(['0:a:one', '1:b:one']);
301 +
302 + const root = ReactDOM.unstable_createRoot(container, {
303 + hydrate: true,
304 + hydrationOptions: {
305 + mutableSources: [mutableSource],
306 + },
307 + });
308 + expect(() => {
309 + act(() => {
310 + root.render(
311 + <>
312 + <Component
313 + label="0"
314 + getSnapshot={getSnapshotA}
315 + mutableSource={mutableSource}
316 + subscribe={subscribeA}
317 + />
318 + <Component
319 + label="1"
320 + getSnapshot={getSnapshotB}
321 + mutableSource={mutableSource}
322 + subscribe={subscribeB}
323 + />
324 + </>,
325 + );
326 + expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
327 + source.valueB = 'b:two';
328 + });
329 + }).toErrorDev(
330 + 'Warning: Did not expect server HTML to contain a <div> in <div>.',
331 + {withoutStack: true},
332 + );
333 + expect(Scheduler).toHaveYielded(['0:a:one', '1:b:two']);
334 + });
335 +
336 + // @gate experimental
337 + it('should detect a tear during a higher priority interruption', () => {
338 + const source = createSource('one');
339 + const mutableSource = createMutableSource(source);
340 +
341 + function Unrelated({flag}) {
342 + Scheduler.unstable_yieldValue(flag);
343 + return flag;
344 + }
345 +
346 + function TestComponent({flag}) {
347 + return (
348 + <>
349 + <Unrelated flag={flag} />
350 + <Component
351 + label="a"
352 + getSnapshot={defaultGetSnapshot}
353 + mutableSource={mutableSource}
354 + subscribe={defaultSubscribe}
355 + />
356 + </>
357 + );
358 + }
359 +
360 + const container = document.createElement('div');
361 + document.body.appendChild(container);
362 +
363 + const htmlString = ReactDOMServer.renderToString(
364 + <TestComponent flag={1} />,
365 + );
366 + container.innerHTML = htmlString;
367 + expect(Scheduler).toHaveYielded([1, 'a:one']);
368 + expect(source.listenerCount).toBe(0);
369 +
370 + const root = ReactDOM.unstable_createRoot(container, {
371 + hydrate: true,
372 + hydrationOptions: {
373 + mutableSources: [mutableSource],
374 + },
375 + });
376 + expect(() => {
377 + act(() => {
378 + root.render(<TestComponent flag={1} />);
379 + expect(Scheduler).toFlushAndYieldThrough([1]);
380 +
381 + // Render an update which will be higher priority than the hydration.
382 + Scheduler.unstable_runWithPriority(
383 + Scheduler.unstable_UserBlockingPriority,
384 + () => root.render(<TestComponent flag={2} />),
385 + );
386 + expect(Scheduler).toFlushAndYieldThrough([2]);
387 +
388 + source.value = 'two';
389 + });
390 + }).toErrorDev(
391 + 'Warning: Text content did not match. Server: "1" Client: "2"',
392 + );
393 + expect(Scheduler).toHaveYielded([2, 'a:two']);
394 + expect(source.listenerCount).toBe(1);
395 + });
396 +});