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
+});