Skip to content

Commit

Permalink
useMutableSource hydration support
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed May 8, 2020
1 parent df14b5b commit 1005e54
Show file tree
Hide file tree
Showing 15 changed files with 618 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1445,8 +1445,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 @@ -1462,25 +1460,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 @@ -1500,7 +1488,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 @@ -1606,9 +1596,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 @@ -1694,14 +1682,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 @@ -1718,12 +1704,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 @@ -1732,7 +1713,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 @@ -1749,12 +1730,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 @@ -1763,7 +1739,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 @@ -1779,12 +1755,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 @@ -1793,7 +1764,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 @@ -1806,12 +1777,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 @@ -1820,7 +1786,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 @@ -1842,16 +1808,14 @@ describe('ReactDOMServerHooks', () => {
<App />,
);

if (
gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
) {
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.
// This error isn't surfaced to the user; only the warning is.
// The error is just the mechanism that restarts the render.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
Expand All @@ -1864,7 +1828,7 @@ describe('ReactDOMServerHooks', () => {
}
});

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 @@ -1888,16 +1852,14 @@ describe('ReactDOMServerHooks', () => {
<App />,
);

if (
gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
) {
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.
// This error isn't surfaced to the user; only the warning is.
// The error is just the mechanism that restarts the render.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
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 @@ -115,6 +116,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,8 +198,12 @@ import {
markSkippedUpdateLanes,
getWorkInProgressRoot,
pushRenderLanes,
getExecutionContext,
RetryAfterError,
NoContext,
} from './ReactFiberWorkLoop.new';
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
import {setWorkInProgressVersion} from './ReactMutableSource.new';

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

Expand Down Expand Up @@ -1061,6 +1066,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 @@ -2263,6 +2278,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,8 +180,12 @@ import {
renderDidSuspendDelayIfPossible,
markUnprocessedUpdateTime,
getWorkInProgressRoot,
getExecutionContext,
RetryAfterError,
NoContext,
} from './ReactFiberWorkLoop.old';
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
import {setWorkInProgressVersion} from './ReactMutableSource.old';

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

Expand Down Expand Up @@ -1038,6 +1043,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 @@ -2239,6 +2254,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/ReactFiberRoot.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;

this.finishedLanes = NoLanes;

this.mutableSourceEagerHydrationData = [];

if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
Expand Down
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberRoot.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.lastPingedTime = NoWork;
this.lastExpiredTime = NoWork;
this.mutableSourceLastPendingUpdateTime = NoWork;
this.mutableSourceEagerHydrationData = [];

if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
Expand Down
Loading

0 comments on commit 1005e54

Please sign in to comment.