Skip to content

Commit a7832bd

Browse files
author
Brian Vaughn
committed
useMutableSource hydration support (DRAFT)
1 parent 3cde22a commit a7832bd

19 files changed

+626
-121
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,
@@ -112,6 +113,13 @@ ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = functi
112113
});
113114
};
114115

116+
ReactDOMRoot.prototype.registerMutableSourceForHydration = ReactDOMBlockingRoot.prototype.registerMutableSourceForHydration = function(
117+
mutableSource: MutableSource<any>,
118+
): void {
119+
const root = this._internalRoot;
120+
registerMutableSourceForHydration(root, mutableSource);
121+
};
122+
115123
function createRootImpl(
116124
container: Container,
117125
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,7 +198,11 @@ import {
197198
markSkippedUpdateLanes,
198199
getWorkInProgressRoot,
199200
pushRenderLanes,
201+
getExecutionContext,
202+
RetryAfterError,
203+
NoContext,
200204
} from './ReactFiberWorkLoop.new';
205+
import {setWorkInProgressVersion} from './ReactMutableSource.new';
201206

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

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

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

2280+
if ((getExecutionContext() & RetryAfterError) !== NoContext) {
2281+
return retrySuspenseComponentWithoutHydrating(
2282+
current,
2283+
workInProgress,
2284+
renderLanes,
2285+
);
2286+
}
2287+
22652288
if ((workInProgress.mode & BlockingMode) === NoMode) {
22662289
return retrySuspenseComponentWithoutHydrating(
22672290
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,7 +180,11 @@ import {
179180
renderDidSuspendDelayIfPossible,
180181
markUnprocessedUpdateTime,
181182
getWorkInProgressRoot,
183+
getExecutionContext,
184+
RetryAfterError,
185+
NoContext,
182186
} from './ReactFiberWorkLoop.old';
187+
import {setWorkInProgressVersion} from './ReactMutableSource.old';
183188

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

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

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

2254+
if ((getExecutionContext() & RetryAfterError) !== NoContext) {
2255+
return retrySuspenseComponentWithoutHydrating(
2256+
current,
2257+
workInProgress,
2258+
renderExpirationTime,
2259+
);
2260+
}
2261+
22392262
if ((workInProgress.mode & BlockingMode) === NoMode) {
22402263
return retrySuspenseComponentWithoutHydrating(
22412264
current,

packages/react-reconciler/src/ReactFiberCompleteWork.new.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,8 @@ function completeWork(
670670
case HostRoot: {
671671
popHostContainer(workInProgress);
672672
popTopLevelLegacyContextObject(workInProgress);
673-
resetMutableSourceWorkInProgressVersions();
673+
const root: FiberRoot = workInProgress.stateNode;
674+
resetMutableSourceWorkInProgressVersions(root);
674675
const fiberRoot = (workInProgress.stateNode: FiberRoot);
675676
if (fiberRoot.pendingContext) {
676677
fiberRoot.context = fiberRoot.pendingContext;

packages/react-reconciler/src/ReactFiberCompleteWork.old.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,8 @@ function completeWork(
666666
case HostRoot: {
667667
popHostContainer(workInProgress);
668668
popTopLevelLegacyContextObject(workInProgress);
669-
resetMutableSourceWorkInProgressVersions();
669+
const root: FiberRoot = workInProgress.stateNode;
670+
resetMutableSourceWorkInProgressVersions(root);
670671
const fiberRoot = (workInProgress.stateNode: FiberRoot);
671672
if (fiberRoot.pendingContext) {
672673
fiberRoot.context = fiberRoot.pendingContext;

0 commit comments

Comments
 (0)