Skip to content

Commit

Permalink
useMutableSource hydration support (DRAFT)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed May 5, 2020
1 parent 3cde22a commit a7832bd
Show file tree
Hide file tree
Showing 19 changed files with 626 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1289,8 +1289,6 @@ describe('ReactDOMServerHooks', () => {
.getAttribute('id');
expect(serverId).not.toBeNull();

const childOneSpan = container.getElementsByTagName('span')[0];

const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App show={false} />);
expect(Scheduler).toHaveYielded([]);
Expand All @@ -1306,25 +1304,15 @@ describe('ReactDOMServerHooks', () => {
// State update should trigger the ID to update, which changes the props
// of ChildWithID. This should cause ChildWithID to hydrate before Children

expect(Scheduler).toFlushAndYieldThrough(
__DEV__
? [
'Child with ID',
// Fallbacks are immediately committed in TestUtils version
// of act
// 'Child with ID',
// 'Child with ID',
'Child One',
'Child Two',
]
: [
'Child with ID',
'Child with ID',
'Child with ID',
'Child One',
'Child Two',
],
);
expect(Scheduler).toFlushAndYieldThrough([
'Child with ID',
// Fallbacks are immediately committed in TestUtils version
// of act
// 'Child with ID',
// 'Child with ID',
'Child One',
'Child Two',
]);

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

// Children hydrates after ChildWithID
expect(child1Ref.current).toBe(childOneSpan);
expect(child1Ref.current).toBe(
container.getElementsByTagName('span')[0],
);

Scheduler.unstable_flushAll();

Expand Down Expand Up @@ -1450,9 +1440,7 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
<App />,
);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(),
).toErrorDev([
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'Warning: Expected server HTML to contain a matching <div> in <div>.',
]);
});
Expand Down Expand Up @@ -1538,14 +1526,12 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
<App />,
);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(),
).toErrorDev([
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'Warning: Expected server HTML to contain a matching <div> in <div>.',
]);
});

it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
function Child({appId}) {
return <div aria-labelledby={appId + ''} />;
}
Expand All @@ -1562,12 +1548,7 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
<App />,
);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev(
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
[
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
'Warning: Did not expect server HTML to contain a <span> in <div>.',
Expand All @@ -1576,7 +1557,7 @@ describe('ReactDOMServerHooks', () => {
);
});

it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
function Child({appId}) {
return <div aria-labelledby={appId + ''} />;
}
Expand All @@ -1593,12 +1574,7 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
<App />,
);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev(
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
[
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
'Warning: Did not expect server HTML to contain a <span> in <div>.',
Expand All @@ -1607,7 +1583,7 @@ describe('ReactDOMServerHooks', () => {
);
});

it('useOpaqueIdentifier throws if you try to use the result as a string in a child component', async () => {
it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => {
function Child({appId}) {
return <div aria-labelledby={appId + ''} />;
}
Expand All @@ -1623,12 +1599,7 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
<App />,
);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev(
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
[
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
'Warning: Did not expect server HTML to contain a <div> in <div>.',
Expand All @@ -1637,7 +1608,7 @@ describe('ReactDOMServerHooks', () => {
);
});

it('useOpaqueIdentifier throws if you try to use the result as a string', async () => {
it('useOpaqueIdentifier warns if you try to use the result as a string', async () => {
function App() {
const id = useOpaqueIdentifier();
return <div aria-labelledby={id + ''} />;
Expand All @@ -1650,12 +1621,7 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
<App />,
);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev(
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
[
'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
'Warning: Did not expect server HTML to contain a <div> in <div>.',
Expand All @@ -1664,7 +1630,7 @@ describe('ReactDOMServerHooks', () => {
);
});

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

if (gate(flags => flags.new)) {
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
} else {
// In the old reconciler, the error isn't surfaced to the user. That
// part isn't important, as long as It warns.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
}
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
});

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

if (gate(flags => flags.new)) {
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
} else {
// In the old reconciler, the error isn't surfaced to the user. That
// part isn't important, as long as It warns.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
}
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
});

it('useOpaqueIdentifier with two opaque identifiers on the same page', () => {
Expand Down
12 changes: 10 additions & 2 deletions packages/react-dom/src/client/ReactDOMRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@

import type {Container} from './ReactDOMHostConfig';
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {MutableSource, ReactNodeList} from 'shared/ReactTypes';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import {findHostInstanceWithNoPortals} from 'react-reconciler/src/ReactFiberReconciler';

export type RootType = {
render(children: ReactNodeList): void,
Expand All @@ -30,6 +29,8 @@ export type RootOptions = {
...
};

import {findHostInstanceWithNoPortals} from 'react-reconciler/src/ReactFiberReconciler';
import {registerMutableSourceForHydration} from 'react-reconciler/src/ReactMutableSource';
import {
isContainerMarkedAsRoot,
markContainerAsRoot,
Expand Down Expand Up @@ -112,6 +113,13 @@ ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = functi
});
};

ReactDOMRoot.prototype.registerMutableSourceForHydration = ReactDOMBlockingRoot.prototype.registerMutableSourceForHydration = function(
mutableSource: MutableSource<any>,
): void {
const root = this._internalRoot;
registerMutableSourceForHydration(root, mutableSource);
};

function createRootImpl(
container: Container,
tag: RootTag,
Expand Down
23 changes: 23 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane';
import type {MutableSource} from 'shared/ReactTypes';
import type {
SuspenseState,
SuspenseListRenderState,
Expand Down Expand Up @@ -197,7 +198,11 @@ import {
markSkippedUpdateLanes,
getWorkInProgressRoot,
pushRenderLanes,
getExecutionContext,
RetryAfterError,
NoContext,
} from './ReactFiberWorkLoop.new';
import {setWorkInProgressVersion} from './ReactMutableSource.new';

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

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

const mutableSourceEagerHydrationData =
root.mutableSourceEagerHydrationData;
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
const mutableSource = ((mutableSourceEagerHydrationData[
i
]: any): MutableSource<any>);
const version = mutableSourceEagerHydrationData[i + 1];
setWorkInProgressVersion(mutableSource, version);
}

const child = mountChildFibers(
workInProgress,
null,
Expand Down Expand Up @@ -2262,6 +2277,14 @@ function updateDehydratedSuspenseComponent(
// but after we've already committed once.
warnIfHydrating();

if ((getExecutionContext() & RetryAfterError) !== NoContext) {
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
);
}

if ((workInProgress.mode & BlockingMode) === NoMode) {
return retrySuspenseComponentWithoutHydrating(
current,
Expand Down
23 changes: 23 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
import type {MutableSource} from 'shared/ReactTypes';
import type {
SuspenseState,
SuspenseListRenderState,
Expand Down Expand Up @@ -179,7 +180,11 @@ import {
renderDidSuspendDelayIfPossible,
markUnprocessedUpdateTime,
getWorkInProgressRoot,
getExecutionContext,
RetryAfterError,
NoContext,
} from './ReactFiberWorkLoop.old';
import {setWorkInProgressVersion} from './ReactMutableSource.old';

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

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

const mutableSourceEagerHydrationData =
root.mutableSourceEagerHydrationData;
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
const mutableSource = ((mutableSourceEagerHydrationData[
i
]: any): MutableSource<any>);
const version = mutableSourceEagerHydrationData[i + 1];
setWorkInProgressVersion(mutableSource, version);
}

const child = mountChildFibers(
workInProgress,
null,
Expand Down Expand Up @@ -2236,6 +2251,14 @@ function updateDehydratedSuspenseComponent(
// but after we've already committed once.
warnIfHydrating();

if ((getExecutionContext() & RetryAfterError) !== NoContext) {
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderExpirationTime,
);
}

if ((workInProgress.mode & BlockingMode) === NoMode) {
return retrySuspenseComponentWithoutHydrating(
current,
Expand Down
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,8 @@ function completeWork(
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const root: FiberRoot = workInProgress.stateNode;
resetMutableSourceWorkInProgressVersions(root);
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
Expand Down
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactFiberCompleteWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,8 @@ function completeWork(
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const root: FiberRoot = workInProgress.stateNode;
resetMutableSourceWorkInProgressVersions(root);
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
Expand Down
Loading

0 comments on commit a7832bd

Please sign in to comment.