Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2e47d3a

Browse files
author
Brian Vaughn
committedMay 6, 2020
useMutableSource hydration support
1 parent a71aa80 commit 2e47d3a

15 files changed

+620
-111
lines changed
 

‎packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js

+32-94
Original file line numberDiff line numberDiff line change
@@ -1289,8 +1289,6 @@ describe('ReactDOMServerHooks', () => {
12891289
.getAttribute('id');
12901290
expect(serverId).not.toBeNull();
12911291

1292-
const childOneSpan = container.getElementsByTagName('span')[0];
1293-
12941292
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
12951293
root.render(<App show={false} />);
12961294
expect(Scheduler).toHaveYielded([]);
@@ -1306,25 +1304,15 @@ describe('ReactDOMServerHooks', () => {
13061304
// State update should trigger the ID to update, which changes the props
13071305
// of ChildWithID. This should cause ChildWithID to hydrate before Children
13081306

1309-
expect(Scheduler).toFlushAndYieldThrough(
1310-
__DEV__
1311-
? [
1312-
'Child with ID',
1313-
// Fallbacks are immediately committed in TestUtils version
1314-
// of act
1315-
// 'Child with ID',
1316-
// 'Child with ID',
1317-
'Child One',
1318-
'Child Two',
1319-
]
1320-
: [
1321-
'Child with ID',
1322-
'Child with ID',
1323-
'Child with ID',
1324-
'Child One',
1325-
'Child Two',
1326-
],
1327-
);
1307+
expect(Scheduler).toFlushAndYieldThrough([
1308+
'Child with ID',
1309+
// Fallbacks are immediately committed in TestUtils version
1310+
// of act
1311+
// 'Child with ID',
1312+
// 'Child with ID',
1313+
'Child One',
1314+
'Child Two',
1315+
]);
13281316

13291317
expect(child1Ref.current).toBe(null);
13301318
expect(childWithIDRef.current).toEqual(
@@ -1344,7 +1332,9 @@ describe('ReactDOMServerHooks', () => {
13441332
});
13451333

13461334
// Children hydrates after ChildWithID
1347-
expect(child1Ref.current).toBe(childOneSpan);
1335+
expect(child1Ref.current).toBe(
1336+
container.getElementsByTagName('span')[0],
1337+
);
13481338

13491339
Scheduler.unstable_flushAll();
13501340

@@ -1450,9 +1440,7 @@ describe('ReactDOMServerHooks', () => {
14501440
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
14511441
<App />,
14521442
);
1453-
expect(() =>
1454-
expect(() => Scheduler.unstable_flushAll()).toThrow(),
1455-
).toErrorDev([
1443+
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
14561444
'Warning: Expected server HTML to contain a matching <div> in <div>.',
14571445
]);
14581446
});
@@ -1538,14 +1526,12 @@ describe('ReactDOMServerHooks', () => {
15381526
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
15391527
<App />,
15401528
);
1541-
expect(() =>
1542-
expect(() => Scheduler.unstable_flushAll()).toThrow(),
1543-
).toErrorDev([
1529+
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
15441530
'Warning: Expected server HTML to contain a matching <div> in <div>.',
15451531
]);
15461532
});
15471533

1548-
it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1534+
it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
15491535
function Child({appId}) {
15501536
return <div aria-labelledby={appId + ''} />;
15511537
}
@@ -1562,12 +1548,7 @@ describe('ReactDOMServerHooks', () => {
15621548
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
15631549
<App />,
15641550
);
1565-
expect(() =>
1566-
expect(() => Scheduler.unstable_flushAll()).toThrow(
1567-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1568-
'Do not read the value directly.',
1569-
),
1570-
).toErrorDev(
1551+
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
15711552
[
15721553
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
15731554
'Warning: Did not expect server HTML to contain a <span> in <div>.',
@@ -1576,7 +1557,7 @@ describe('ReactDOMServerHooks', () => {
15761557
);
15771558
});
15781559

1579-
it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1560+
it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
15801561
function Child({appId}) {
15811562
return <div aria-labelledby={appId + ''} />;
15821563
}
@@ -1593,12 +1574,7 @@ describe('ReactDOMServerHooks', () => {
15931574
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
15941575
<App />,
15951576
);
1596-
expect(() =>
1597-
expect(() => Scheduler.unstable_flushAll()).toThrow(
1598-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1599-
'Do not read the value directly.',
1600-
),
1601-
).toErrorDev(
1577+
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
16021578
[
16031579
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
16041580
'Warning: Did not expect server HTML to contain a <span> in <div>.',
@@ -1607,7 +1583,7 @@ describe('ReactDOMServerHooks', () => {
16071583
);
16081584
});
16091585

1610-
it('useOpaqueIdentifier throws if you try to use the result as a string in a child component', async () => {
1586+
it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => {
16111587
function Child({appId}) {
16121588
return <div aria-labelledby={appId + ''} />;
16131589
}
@@ -1623,12 +1599,7 @@ describe('ReactDOMServerHooks', () => {
16231599
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
16241600
<App />,
16251601
);
1626-
expect(() =>
1627-
expect(() => Scheduler.unstable_flushAll()).toThrow(
1628-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1629-
'Do not read the value directly.',
1630-
),
1631-
).toErrorDev(
1602+
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
16321603
[
16331604
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
16341605
'Warning: Did not expect server HTML to contain a <div> in <div>.',
@@ -1637,7 +1608,7 @@ describe('ReactDOMServerHooks', () => {
16371608
);
16381609
});
16391610

1640-
it('useOpaqueIdentifier throws if you try to use the result as a string', async () => {
1611+
it('useOpaqueIdentifier warns if you try to use the result as a string', async () => {
16411612
function App() {
16421613
const id = useOpaqueIdentifier();
16431614
return <div aria-labelledby={id + ''} />;
@@ -1650,12 +1621,7 @@ describe('ReactDOMServerHooks', () => {
16501621
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
16511622
<App />,
16521623
);
1653-
expect(() =>
1654-
expect(() => Scheduler.unstable_flushAll()).toThrow(
1655-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1656-
'Do not read the value directly.',
1657-
),
1658-
).toErrorDev(
1624+
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
16591625
[
16601626
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
16611627
'Warning: Did not expect server HTML to contain a <div> in <div>.',
@@ -1664,7 +1630,7 @@ describe('ReactDOMServerHooks', () => {
16641630
);
16651631
});
16661632

1667-
it('useOpaqueIdentifier throws if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
1633+
it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
16681634
function Child({appId}) {
16691635
return <div aria-labelledby={appId + ''} />;
16701636
}
@@ -1686,27 +1652,13 @@ describe('ReactDOMServerHooks', () => {
16861652
<App />,
16871653
);
16881654

1689-
if (gate(flags => flags.new)) {
1690-
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1691-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1692-
'Do not read the value directly.',
1693-
]);
1694-
} else {
1695-
// In the old reconciler, the error isn't surfaced to the user. That
1696-
// part isn't important, as long as It warns.
1697-
expect(() =>
1698-
expect(() => Scheduler.unstable_flushAll()).toThrow(
1699-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1700-
'Do not read the value directly.',
1701-
),
1702-
).toErrorDev([
1703-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1704-
'Do not read the value directly.',
1705-
]);
1706-
}
1655+
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1656+
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1657+
'Do not read the value directly.',
1658+
]);
17071659
});
17081660

1709-
it('useOpaqueIdentifier throws if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
1661+
it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
17101662
function Child({appId}) {
17111663
return <div aria-labelledby={+appId} />;
17121664
}
@@ -1730,24 +1682,10 @@ describe('ReactDOMServerHooks', () => {
17301682
<App />,
17311683
);
17321684

1733-
if (gate(flags => flags.new)) {
1734-
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1735-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1736-
'Do not read the value directly.',
1737-
]);
1738-
} else {
1739-
// In the old reconciler, the error isn't surfaced to the user. That
1740-
// part isn't important, as long as It warns.
1741-
expect(() =>
1742-
expect(() => Scheduler.unstable_flushAll()).toThrow(
1743-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1744-
'Do not read the value directly.',
1745-
),
1746-
).toErrorDev([
1747-
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1748-
'Do not read the value directly.',
1749-
]);
1750-
}
1685+
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
1686+
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1687+
'Do not read the value directly.',
1688+
]);
17511689
});
17521690

17531691
it('useOpaqueIdentifier with two opaque identifiers on the same page', () => {

‎packages/react-dom/src/client/ReactDOMRoot.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99

1010
import type {Container} from './ReactDOMHostConfig';
1111
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
12-
import type {ReactNodeList} from 'shared/ReactTypes';
12+
import type {MutableSource, ReactNodeList} from 'shared/ReactTypes';
1313
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
14-
import {findHostInstanceWithNoPortals} from 'react-reconciler/src/ReactFiberReconciler';
1514

1615
export type RootType = {
1716
render(children: ReactNodeList): void,
@@ -30,6 +29,8 @@ export type RootOptions = {
3029
...
3130
};
3231

32+
import {findHostInstanceWithNoPortals} from 'react-reconciler/src/ReactFiberReconciler';
33+
import {registerMutableSourceForHydration} from 'react-reconciler/src/ReactMutableSource';
3334
import {
3435
isContainerMarkedAsRoot,
3536
markContainerAsRoot,
@@ -115,6 +116,13 @@ ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = functi
115116
});
116117
};
117118

119+
ReactDOMRoot.prototype.registerMutableSourceForHydration = ReactDOMBlockingRoot.prototype.registerMutableSourceForHydration = function(
120+
mutableSource: MutableSource<any>,
121+
): void {
122+
const root = this._internalRoot;
123+
registerMutableSourceForHydration(root, mutableSource);
124+
};
125+
118126
function createRootImpl(
119127
container: Container,
120128
tag: RootTag,

‎packages/react-reconciler/src/ReactFiberBeginWork.new.js

+23
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
1313
import type {Fiber} from './ReactInternalTypes';
1414
import type {FiberRoot} from './ReactInternalTypes';
1515
import type {Lanes, Lane} from './ReactFiberLane';
16+
import type {MutableSource} from 'shared/ReactTypes';
1617
import type {
1718
SuspenseState,
1819
SuspenseListRenderState,
@@ -197,8 +198,12 @@ import {
197198
markSkippedUpdateLanes,
198199
getWorkInProgressRoot,
199200
pushRenderLanes,
201+
getExecutionContext,
202+
RetryAfterError,
203+
NoContext,
200204
} from './ReactFiberWorkLoop.new';
201205
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
206+
import {setWorkInProgressVersion} from './ReactMutableSource.new';
202207

203208
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
204209

@@ -1061,6 +1066,16 @@ function updateHostRoot(current, workInProgress, renderLanes) {
10611066
// be any children to hydrate which is effectively the same thing as
10621067
// not hydrating.
10631068

1069+
const mutableSourceEagerHydrationData =
1070+
root.mutableSourceEagerHydrationData;
1071+
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1072+
const mutableSource = ((mutableSourceEagerHydrationData[
1073+
i
1074+
]: any): MutableSource<any>);
1075+
const version = mutableSourceEagerHydrationData[i + 1];
1076+
setWorkInProgressVersion(mutableSource, version);
1077+
}
1078+
10641079
const child = mountChildFibers(
10651080
workInProgress,
10661081
null,
@@ -2263,6 +2278,14 @@ function updateDehydratedSuspenseComponent(
22632278
// but after we've already committed once.
22642279
warnIfHydrating();
22652280

2281+
if ((getExecutionContext() & RetryAfterError) !== NoContext) {
2282+
return retrySuspenseComponentWithoutHydrating(
2283+
current,
2284+
workInProgress,
2285+
renderLanes,
2286+
);
2287+
}
2288+
22662289
if ((workInProgress.mode & BlockingMode) === NoMode) {
22672290
return retrySuspenseComponentWithoutHydrating(
22682291
current,

‎packages/react-reconciler/src/ReactFiberBeginWork.old.js

+23
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
1313
import type {Fiber} from './ReactInternalTypes';
1414
import type {FiberRoot} from './ReactInternalTypes';
1515
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
16+
import type {MutableSource} from 'shared/ReactTypes';
1617
import type {
1718
SuspenseState,
1819
SuspenseListRenderState,
@@ -179,8 +180,12 @@ import {
179180
renderDidSuspendDelayIfPossible,
180181
markUnprocessedUpdateTime,
181182
getWorkInProgressRoot,
183+
getExecutionContext,
184+
RetryAfterError,
185+
NoContext,
182186
} from './ReactFiberWorkLoop.old';
183187
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
188+
import {setWorkInProgressVersion} from './ReactMutableSource.old';
184189

185190
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
186191

@@ -1038,6 +1043,16 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
10381043
// be any children to hydrate which is effectively the same thing as
10391044
// not hydrating.
10401045

1046+
const mutableSourceEagerHydrationData =
1047+
root.mutableSourceEagerHydrationData;
1048+
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1049+
const mutableSource = ((mutableSourceEagerHydrationData[
1050+
i
1051+
]: any): MutableSource<any>);
1052+
const version = mutableSourceEagerHydrationData[i + 1];
1053+
setWorkInProgressVersion(mutableSource, version);
1054+
}
1055+
10411056
const child = mountChildFibers(
10421057
workInProgress,
10431058
null,
@@ -2239,6 +2254,14 @@ function updateDehydratedSuspenseComponent(
22392254
// but after we've already committed once.
22402255
warnIfHydrating();
22412256

2257+
if ((getExecutionContext() & RetryAfterError) !== NoContext) {
2258+
return retrySuspenseComponentWithoutHydrating(
2259+
current,
2260+
workInProgress,
2261+
renderExpirationTime,
2262+
);
2263+
}
2264+
22422265
if ((workInProgress.mode & BlockingMode) === NoMode) {
22432266
return retrySuspenseComponentWithoutHydrating(
22442267
current,

‎packages/react-reconciler/src/ReactFiberRoot.new.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ function FiberRootNode(containerInfo, tag, hydrate) {
4141
this.pingedLanes = NoLanes;
4242
this.expiredLanes = NoLanes;
4343
this.mutableReadLanes = NoLanes;
44-
4544
this.finishedLanes = NoLanes;
4645

46+
this.mutableSourceEagerHydrationData = [];
47+
4748
if (enableSchedulerTracing) {
4849
this.interactionThreadID = unstable_getThreadID();
4950
this.memoizedInteractions = new Set();

‎packages/react-reconciler/src/ReactFiberRoot.old.js

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
4545
this.lastPingedTime = NoWork;
4646
this.lastExpiredTime = NoWork;
4747
this.mutableSourceLastPendingUpdateTime = NoWork;
48+
this.mutableSourceEagerHydrationData = [];
4849

4950
if (enableSchedulerTracing) {
5051
this.interactionThreadID = unstable_getThreadID();

‎packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+31-7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
warnsIfNotActing,
6262
beforeActiveInstanceBlur,
6363
afterActiveInstanceBlur,
64+
clearContainer,
6465
} from './ReactFiberHostConfig';
6566

6667
import {
@@ -210,13 +211,14 @@ const {
210211

211212
type ExecutionContext = number;
212213

213-
const NoContext = /* */ 0b000000;
214-
const BatchedContext = /* */ 0b000001;
215-
const EventContext = /* */ 0b000010;
216-
const DiscreteEventContext = /* */ 0b000100;
217-
const LegacyUnbatchedContext = /* */ 0b001000;
218-
const RenderContext = /* */ 0b010000;
219-
const CommitContext = /* */ 0b100000;
214+
export const NoContext = /* */ 0b0000000;
215+
const BatchedContext = /* */ 0b0000001;
216+
const EventContext = /* */ 0b0000010;
217+
const DiscreteEventContext = /* */ 0b0000100;
218+
const LegacyUnbatchedContext = /* */ 0b0001000;
219+
const RenderContext = /* */ 0b0010000;
220+
const CommitContext = /* */ 0b0100000;
221+
export const RetryAfterError = /* */ 0b1000000;
220222

221223
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
222224
const RootIncomplete = 0;
@@ -703,6 +705,15 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
703705
prepareFreshStack(root, NoLanes);
704706
} else if (exitStatus !== RootIncomplete) {
705707
if (exitStatus === RootErrored) {
708+
executionContext |= RetryAfterError;
709+
710+
// If an error occurred during hydration,
711+
// discard server response and fall back to client side render.
712+
if (root.hydrate) {
713+
root.hydrate = false;
714+
clearContainer(root.containerInfo);
715+
}
716+
706717
// If something threw an error, try rendering one more time. We'll render
707718
// synchronously to block concurrent data mutations, and we'll includes
708719
// all pending updates are included. If it still fails after the second
@@ -953,6 +964,15 @@ function performSyncWorkOnRoot(root) {
953964
}
954965

955966
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
967+
executionContext |= RetryAfterError;
968+
969+
// If an error occurred during hydration,
970+
// discard server response and fall back to client side render.
971+
if (root.hydrate) {
972+
root.hydrate = false;
973+
clearContainer(root.containerInfo);
974+
}
975+
956976
// If something threw an error, try rendering one more time. We'll render
957977
// synchronously to block concurrent data mutations, and we'll includes
958978
// all pending updates are included. If it still fails after the second
@@ -993,6 +1013,10 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
9931013
}
9941014
}
9951015

1016+
export function getExecutionContext(): ExecutionContext {
1017+
return executionContext;
1018+
}
1019+
9961020
export function flushDiscreteUpdates() {
9971021
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
9981022
// However, `act` uses `batchedUpdates`, so there's no way to distinguish

‎packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+31-7
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import {
7474
warnsIfNotActing,
7575
beforeActiveInstanceBlur,
7676
afterActiveInstanceBlur,
77+
clearContainer,
7778
} from './ReactFiberHostConfig';
7879

7980
import {
@@ -207,13 +208,14 @@ const {
207208

208209
type ExecutionContext = number;
209210

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;
217219

218220
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
219221
const RootIncomplete = 0;
@@ -728,6 +730,15 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
728730

729731
if (exitStatus !== RootIncomplete) {
730732
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+
731742
// If something threw an error, try rendering one more time. We'll
732743
// render synchronously to block concurrent data mutations, and we'll
733744
// render at Idle (or lower) so that all pending updates are included.
@@ -1011,6 +1022,15 @@ function performSyncWorkOnRoot(root) {
10111022
let exitStatus = renderRootSync(root, expirationTime);
10121023

10131024
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+
10141034
// If something threw an error, try rendering one more time. We'll
10151035
// render synchronously to block concurrent data mutations, and we'll
10161036
// render at Idle (or lower) so that all pending updates are included.
@@ -1051,6 +1071,10 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
10511071
}
10521072
}
10531073

1074+
export function getExecutionContext(): ExecutionContext {
1075+
return executionContext;
1076+
}
1077+
10541078
export function flushDiscreteUpdates() {
10551079
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
10561080
// However, `act` uses `batchedUpdates`, so there's no way to distinguish

‎packages/react-reconciler/src/ReactInternalTypes.js

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
ReactContext,
1717
MutableSourceSubscribeFn,
1818
MutableSourceGetSnapshotFn,
19+
MutableSourceVersion,
1920
MutableSource,
2021
} from 'shared/ReactTypes';
2122
import type {SuspenseInstance} from './ReactFiberHostConfig';
@@ -248,6 +249,11 @@ type BaseFiberRootProperties = {|
248249
// when external, mutable sources are read from during render.
249250
mutableSourceLastPendingUpdateTime: ExpirationTime,
250251

252+
// Used by useMutableSource hook to avoid tearing during hydrtaion.
253+
mutableSourceEagerHydrationData: Array<
254+
MutableSource<any> | MutableSourceVersion,
255+
>,
256+
251257
// Only used by new reconciler
252258

253259
// Represents the next task that the root should work on, or the current one
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
* @flow
8+
*/
9+
10+
import {enableNewReconciler} from 'shared/ReactFeatureFlags';
11+
12+
// The entry file imports either the old or new version of mutable source.
13+
// This is necessary since ReactDOMRoot imports this module directly.
14+
// Note that it's not possible to export all of the API methods,
15+
// as the new and old implementations fork slightly (due to the lanes refactor).
16+
// It's only necessary to export the subset of the API required by ReactDOMRoot.
17+
18+
import {registerMutableSourceForHydration as registerMutableSourceForHydration_old} from './ReactMutableSource.old';
19+
20+
import {registerMutableSourceForHydration as registerMutableSourceForHydration_new} from './ReactMutableSource.new';
21+
22+
export const registerMutableSourceForHydration = enableNewReconciler
23+
? registerMutableSourceForHydration_new
24+
: registerMutableSourceForHydration_old;

‎packages/react-reconciler/src/ReactMutableSource.new.js

+17
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes';
11+
import type {FiberRoot} from './ReactInternalTypes';
1112

1213
import {isPrimaryRenderer} from './ReactFiberHostConfig';
1314

@@ -95,3 +96,19 @@ export function warnAboutMultipleRenderersDEV(
9596
}
9697
}
9798
}
99+
100+
// Eager reads the version of a mutable source and stores it on the root.
101+
// This ensures that the version used for server rendering matches the one
102+
// that is eventually read during hydration.
103+
// If they don't match there's a potential tear and a full deopt render is required.
104+
export function registerMutableSourceForHydration(
105+
root: FiberRoot,
106+
mutableSource: MutableSource<any>,
107+
): void {
108+
const getVersion = mutableSource._getVersion;
109+
const version = getVersion(mutableSource._source);
110+
111+
// TODO Clear this data once all pending hydration work is finished.
112+
// Retaining it forever may interfere with GC.
113+
root.mutableSourceEagerHydrationData.push(mutableSource, version);
114+
}

‎packages/react-reconciler/src/ReactMutableSource.old.js

+16
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,19 @@ export function warnAboutMultipleRenderersDEV(
126126
}
127127
}
128128
}
129+
130+
// Eager reads the version of a mutable source and stores it on the root.
131+
// This ensures that the version used for server rendering matches the one
132+
// that is eventually read during hydration.
133+
// If they don't match there's a potential tear and a full deopt render is required.
134+
export function registerMutableSourceForHydration(
135+
root: FiberRoot,
136+
mutableSource: MutableSource<any>,
137+
): void {
138+
const getVersion = mutableSource._getVersion;
139+
const version = getVersion(mutableSource._source);
140+
141+
// TODO Clear this data once all pending hydration work is finished.
142+
// Retaining it forever may interfere with GC.
143+
root.mutableSourceEagerHydrationData.push(mutableSource, version);
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
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, {hydrate: true});
161+
act(() => {
162+
root.registerMutableSourceForHydration(mutableSource);
163+
root.render(<TestComponent />);
164+
});
165+
expect(Scheduler).toHaveYielded(['only:one']);
166+
expect(source.listenerCount).toBe(1);
167+
});
168+
169+
// @gate experimental
170+
it('should detect a tear before hydrating a component', () => {
171+
const source = createSource('one');
172+
const mutableSource = createMutableSource(source);
173+
174+
function TestComponent() {
175+
return (
176+
<Component
177+
label="only"
178+
getSnapshot={defaultGetSnapshot}
179+
mutableSource={mutableSource}
180+
subscribe={defaultSubscribe}
181+
/>
182+
);
183+
}
184+
185+
const container = document.createElement('div');
186+
document.body.appendChild(container);
187+
188+
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
189+
container.innerHTML = htmlString;
190+
expect(Scheduler).toHaveYielded(['only:one']);
191+
expect(source.listenerCount).toBe(0);
192+
193+
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
194+
expect(() => {
195+
act(() => {
196+
root.registerMutableSourceForHydration(mutableSource);
197+
root.render(<TestComponent />);
198+
199+
source.value = 'two';
200+
});
201+
}).toErrorDev(
202+
'Warning: Did not expect server HTML to contain a <div> in <div>.',
203+
{withoutStack: true},
204+
);
205+
expect(Scheduler).toHaveYielded(['only:two']);
206+
expect(source.listenerCount).toBe(1);
207+
});
208+
209+
// @gate experimental
210+
it('should detect a tear between hydrating components', () => {
211+
const source = createSource('one');
212+
const mutableSource = createMutableSource(source);
213+
214+
function TestComponent() {
215+
return (
216+
<>
217+
<Component
218+
label="a"
219+
getSnapshot={defaultGetSnapshot}
220+
mutableSource={mutableSource}
221+
subscribe={defaultSubscribe}
222+
/>
223+
<Component
224+
label="b"
225+
getSnapshot={defaultGetSnapshot}
226+
mutableSource={mutableSource}
227+
subscribe={defaultSubscribe}
228+
/>
229+
</>
230+
);
231+
}
232+
233+
const container = document.createElement('div');
234+
document.body.appendChild(container);
235+
236+
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
237+
container.innerHTML = htmlString;
238+
expect(Scheduler).toHaveYielded(['a:one', 'b:one']);
239+
expect(source.listenerCount).toBe(0);
240+
241+
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
242+
expect(() => {
243+
act(() => {
244+
root.registerMutableSourceForHydration(mutableSource);
245+
root.render(<TestComponent />);
246+
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
247+
source.value = 'two';
248+
});
249+
}).toErrorDev(
250+
'Warning: Did not expect server HTML to contain a <div> in <div>.',
251+
{withoutStack: true},
252+
);
253+
expect(Scheduler).toHaveYielded(['a:two', 'b:two']);
254+
expect(source.listenerCount).toBe(2);
255+
});
256+
257+
// @gate experimental
258+
it('should detect a tear between hydrating components reading from different parts of a source', () => {
259+
const source = createComplexSource('a:one', 'b:one');
260+
const mutableSource = createMutableSource(source);
261+
262+
// Subscribe to part of the store.
263+
const getSnapshotA = s => s.valueA;
264+
const subscribeA = (s, callback) => s.subscribeA(callback);
265+
const getSnapshotB = s => s.valueB;
266+
const subscribeB = (s, callback) => s.subscribeB(callback);
267+
268+
const container = document.createElement('div');
269+
document.body.appendChild(container);
270+
271+
const htmlString = ReactDOMServer.renderToString(
272+
<>
273+
<Component
274+
label="0"
275+
getSnapshot={getSnapshotA}
276+
mutableSource={mutableSource}
277+
subscribe={subscribeA}
278+
/>
279+
<Component
280+
label="1"
281+
getSnapshot={getSnapshotB}
282+
mutableSource={mutableSource}
283+
subscribe={subscribeB}
284+
/>
285+
</>,
286+
);
287+
container.innerHTML = htmlString;
288+
expect(Scheduler).toHaveYielded(['0:a:one', '1:b:one']);
289+
290+
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
291+
expect(() => {
292+
act(() => {
293+
root.registerMutableSourceForHydration(mutableSource);
294+
root.render(
295+
<>
296+
<Component
297+
label="0"
298+
getSnapshot={getSnapshotA}
299+
mutableSource={mutableSource}
300+
subscribe={subscribeA}
301+
/>
302+
<Component
303+
label="1"
304+
getSnapshot={getSnapshotB}
305+
mutableSource={mutableSource}
306+
subscribe={subscribeB}
307+
/>
308+
</>,
309+
);
310+
expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
311+
source.valueB = 'b:two';
312+
});
313+
}).toErrorDev(
314+
'Warning: Did not expect server HTML to contain a <div> in <div>.',
315+
{withoutStack: true},
316+
);
317+
expect(Scheduler).toHaveYielded(['0:a:one', '1:b:two']);
318+
});
319+
320+
// @gate experimental
321+
it('should detect a tear during a higher priority interruption', () => {
322+
const source = createSource('one');
323+
const mutableSource = createMutableSource(source);
324+
325+
function Unrelated({flag}) {
326+
Scheduler.unstable_yieldValue(flag);
327+
return flag;
328+
}
329+
330+
function TestComponent({flag}) {
331+
return (
332+
<>
333+
<Unrelated flag={flag} />
334+
<Component
335+
label="a"
336+
getSnapshot={defaultGetSnapshot}
337+
mutableSource={mutableSource}
338+
subscribe={defaultSubscribe}
339+
/>
340+
</>
341+
);
342+
}
343+
344+
const container = document.createElement('div');
345+
document.body.appendChild(container);
346+
347+
const htmlString = ReactDOMServer.renderToString(
348+
<TestComponent flag={1} />,
349+
);
350+
container.innerHTML = htmlString;
351+
expect(Scheduler).toHaveYielded([1, 'a:one']);
352+
expect(source.listenerCount).toBe(0);
353+
354+
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
355+
expect(() => {
356+
act(() => {
357+
root.registerMutableSourceForHydration(mutableSource);
358+
root.render(<TestComponent flag={1} />);
359+
expect(Scheduler).toFlushAndYieldThrough([1]);
360+
361+
// Render an update which will be higher priority than the hydration.
362+
Scheduler.unstable_runWithPriority(
363+
Scheduler.unstable_UserBlockingPriority,
364+
() => root.render(<TestComponent flag={2} />),
365+
);
366+
expect(Scheduler).toFlushAndYieldThrough([2]);
367+
368+
source.value = 'two';
369+
});
370+
}).toErrorDev(
371+
'Warning: Text content did not match. Server: "1" Client: "2"',
372+
);
373+
expect(Scheduler).toHaveYielded([2, 'a:two']);
374+
expect(source.listenerCount).toBe(1);
375+
});
376+
});

‎scripts/jest/setupHostConfigs.js

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ jest.mock('react-reconciler/src/ReactFiberReconciler', () => {
1010
);
1111
});
1212

13+
jest.mock('react-reconciler/src/ReactMutableSource', () => {
14+
return require.requireActual(
15+
__VARIANT__
16+
? 'react-reconciler/src/ReactMutableSource.new'
17+
: 'react-reconciler/src/ReactMutableSource.old'
18+
);
19+
});
20+
1321
// When testing the custom renderer code path through `react-reconciler`,
1422
// turn the export into a function, and use the argument as host config.
1523
const shimHostConfigPath = 'react-reconciler/src/ReactFiberHostConfig';

‎scripts/rollup/forks.js

+20
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,26 @@ const forks = Object.freeze({
280280
return 'react-reconciler/src/ReactFiberReconciler.old.js';
281281
},
282282

283+
'react-reconciler/src/ReactMutableSource': (
284+
bundleType,
285+
entry,
286+
dependencies,
287+
moduleType,
288+
bundle
289+
) => {
290+
if (bundle.enableNewReconciler) {
291+
switch (bundleType) {
292+
case FB_WWW_DEV:
293+
case FB_WWW_PROD:
294+
case FB_WWW_PROFILING:
295+
// Use the forked version of the reconciler
296+
return 'react-reconciler/src/ReactMutableSource.new.js';
297+
}
298+
}
299+
// Otherwise, use the non-forked version.
300+
return 'react-reconciler/src/ReactMutableSource.old.js';
301+
},
302+
283303
'react-reconciler/src/ReactFiberHotReloading': (
284304
bundleType,
285305
entry,

0 commit comments

Comments
 (0)
Please sign in to comment.