Skip to content

Commit

Permalink
Incremental hydration
Browse files Browse the repository at this point in the history
Stores the tree context on the dehydrated Suspense boundary's state
object so it resume where it left off.
  • Loading branch information
acdlite committed Oct 30, 2021
1 parent 0bb01e4 commit 75a0cc2
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 0 deletions.
131 changes: 131 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMUseId-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
let JSDOM;
let React;
let ReactDOM;
let Scheduler;
let clientAct;
let ReactDOMFizzServer;
let Stream;
let Suspense;
let useId;
let document;
let writable;
Expand All @@ -27,9 +29,11 @@ describe('useId', () => {
JSDOM = require('jsdom').JSDOM;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
clientAct = require('jest-react').act;
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
useId = React.unstable_useId;

// Test Environment
Expand Down Expand Up @@ -86,6 +90,11 @@ describe('useId', () => {
}
}

function Text({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}

function normalizeTreeIdForTesting(id) {
const [serverClientPrefix, base32, hookIndex] = id.split(':');
if (serverClientPrefix === 'r') {
Expand Down Expand Up @@ -282,4 +291,126 @@ describe('useId', () => {
expect(divs[i].id).toMatch(/^R:.(((al)*a?)((la)*l?))*$/);
}
});

test('basic incremental hydration', async () => {
function App() {
return (
<div>
<Suspense fallback="Loading...">
<DivWithId label="A" />
<DivWithId label="B" />
</Suspense>
<DivWithId label="C" />
</div>
);
}

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
<!--$-->
<div
id="101"
/>
<div
id="1001"
/>
<!--/$-->
<div
id="10"
/>
</div>
</div>
`);
});

test('inserting a sibling before a dehydrated Suspense boundary', async () => {
function App({showMore}) {
const siblings = showMore
? [<Text key="A" text="A" />, <Text key="B" text="B" />]
: [<Text key="A" text="A" />];

return (
<div>
{siblings}
<Suspense fallback="Loading...">
<DivWithId label="A" />
<DivWithId label="B" />
</Suspense>
<DivWithId label="C" />
</div>
);
}

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(Scheduler).toHaveYielded(['A']);
await clientAct(async () => {
const root = ReactDOM.hydrateRoot(container, <App />);
expect(Scheduler).toFlushUntilNextPaint(['A']);
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
A
<!-- -->
<!--$-->
<div
id="110"
/>
<div
id="1010"
/>
<!--/$-->
<div
id="11"
/>
</div>
</div>
`);
// Insert another sibling before the Suspense boundary
root.render(<App showMore={true} />);
});
expect(Scheduler).toHaveYielded([
'A',
'B',
// The update triggers selective hydration so we render again
'A',
'B',
]);
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
A
<!-- -->
<!--$-->
B
<div
id="110"
/>
<div
id="1010"
/>
<!--/$-->
<div
id="11"
/>
</div>
</div>
`);
});
});
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {

const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
treeContext: null,
retryLane: NoLane,
};

Expand Down Expand Up @@ -2734,6 +2735,7 @@ function updateDehydratedSuspenseComponent(
reenterHydrationStateFromDehydratedSuspenseInstance(
workInProgress,
suspenseInstance,
suspenseState.treeContext,
);
const nextProps = workInProgress.pendingProps;
const primaryChildren = nextProps.children;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {

const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
treeContext: null,
retryLane: NoLane,
};

Expand Down Expand Up @@ -2734,6 +2735,7 @@ function updateDehydratedSuspenseComponent(
reenterHydrationStateFromDehydratedSuspenseInstance(
workInProgress,
suspenseInstance,
suspenseState.treeContext,
);
const nextProps = workInProgress.pendingProps;
const primaryChildren = nextProps.children;
Expand Down
10 changes: 10 additions & 0 deletions packages/react-reconciler/src/ReactFiberHydrationContext.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
HostContext,
} from './ReactFiberHostConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {TreeContext} from './ReactFiberTreeContext.new';

import {
HostComponent,
Expand Down Expand Up @@ -62,6 +63,10 @@ import {
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.new';
import {
getSuspendedTreeContext,
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.new';

// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
Expand Down Expand Up @@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
function reenterHydrationStateFromDehydratedSuspenseInstance(
fiber: Fiber,
suspenseInstance: SuspenseInstance,
treeContext: TreeContext | null,
): boolean {
if (!supportsHydration) {
return false;
Expand All @@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
return true;
}

Expand Down Expand Up @@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
treeContext: getSuspendedTreeContext(),
retryLane: OffscreenLane,
};
fiber.memoizedState = suspenseState;
Expand Down
10 changes: 10 additions & 0 deletions packages/react-reconciler/src/ReactFiberHydrationContext.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
HostContext,
} from './ReactFiberHostConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import type {TreeContext} from './ReactFiberTreeContext.old';

import {
HostComponent,
Expand Down Expand Up @@ -62,6 +63,10 @@ import {
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.old';
import {
getSuspendedTreeContext,
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.old';

// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
Expand Down Expand Up @@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
function reenterHydrationStateFromDehydratedSuspenseInstance(
fiber: Fiber,
suspenseInstance: SuspenseInstance,
treeContext: TreeContext | null,
): boolean {
if (!supportsHydration) {
return false;
Expand All @@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
return true;
}

Expand Down Expand Up @@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
treeContext: getSuspendedTreeContext(),
retryLane: OffscreenLane,
};
fiber.memoizedState = suspenseState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {Lane} from './ReactFiberLane.new';
import type {TreeContext} from './ReactFiberTreeContext.new';

import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoFlags, DidCapture} from './ReactFiberFlags';
import {
Expand Down Expand Up @@ -40,6 +42,7 @@ export type SuspenseState = {|
// here to indicate that it is dehydrated (flag) and for quick access
// to check things like isSuspenseInstancePending.
dehydrated: null | SuspenseInstance,
treeContext: null | TreeContext,
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {Lane} from './ReactFiberLane.old';
import type {TreeContext} from './ReactFiberTreeContext.old';

import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoFlags, DidCapture} from './ReactFiberFlags';
import {
Expand Down Expand Up @@ -40,6 +42,7 @@ export type SuspenseState = {|
// here to indicate that it is dehydrated (flag) and for quick access
// to check things like isSuspenseInstancePending.
dehydrated: null | SuspenseInstance,
treeContext: null | TreeContext,
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.
Expand Down
37 changes: 37 additions & 0 deletions packages/react-reconciler/src/ReactFiberTreeContext.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ import {getIsHydrating} from './ReactFiberHydrationContext.new';
import {clz32} from './clz32';
import {Forked, NoFlags} from './ReactFiberFlags';

export type TreeContext = {
id: number,
length: number,
overflow: string,
};

let treeContextId: number = 0;
let treeContextLength: number = 0;
let treeContextOverflow: string = '';
Expand Down Expand Up @@ -199,6 +205,37 @@ export function popTreeContext(workInProgress: Fiber) {
}
}

export function getSuspendedTreeContext(): TreeContext | null {
warnIfNotHydrating();

if (treeIdProvider !== null) {
return {
id: treeContextId,
length: treeContextLength,
overflow: treeContextOverflow,
};
} else {
return null;
}
}

export function restoreSuspendedTreeContext(
workInProgress: Fiber,
suspendedContext: TreeContext,
) {
warnIfNotHydrating();

stack[stackIndex++] = treeContextId;
stack[stackIndex++] = treeContextLength;
stack[stackIndex++] = treeContextOverflow;
stack[stackIndex++] = treeIdProvider;

treeContextId = suspendedContext.id;
treeContextLength = suspendedContext.length;
treeContextOverflow = suspendedContext.overflow;
treeIdProvider = workInProgress;
}

function warnIfNotHydrating() {
if (__DEV__) {
if (!getIsHydrating()) {
Expand Down
Loading

0 comments on commit 75a0cc2

Please sign in to comment.