Skip to content

Commit

Permalink
Encode lazy Node slots properly in key path and resumable paths (face…
Browse files Browse the repository at this point in the history
…book#27359)

It's possible to postpone a specific node and not using a wrapper
component. Therefore we encode the resumable slot as the index slot.
When it's a plain client component that postpones, it's encoded as the
child slot inside that component which is the one that's postponed
rather than the component itself.

Since it's possible for a child slot to suspend (e.g. React.lazy's
microtask in this case) retryTask might need to keep its index around
when it resolves.
  • Loading branch information
sebmarkbage authored and AndyPengc12 committed Apr 15, 2024
1 parent 78c93a8 commit 80063a5
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 23 deletions.
41 changes: 41 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,47 @@ describe('ReactDOMFizzStaticBrowser', () => {
// TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});

// @gate enablePostpone
it('supports postponing in lazy in prerender and resuming later', async () => {
let prerendering = true;
const Hole = React.lazy(async () => {
React.unstable_postpone();
});

function Postpone() {
return 'Hello';
}

function App() {
return (
<div>
<Suspense fallback="Loading...">
Hi
{prerendering ? Hole : <Postpone />}
</Suspense>
</div>
);
}

const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);

prerendering = false;

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
);

await readIntoContainer(prerendered.prelude);

expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);

await readIntoContainer(resumed);

// TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});

// @gate enablePostpone
it('only emits end tags once when resuming', async () => {
let prerendering = true;
Expand Down
89 changes: 66 additions & 23 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,7 @@ type ResumableNode =
| ResumableParentNode
| [
2, // RESUME_SEGMENT
string | null /* name */,
string | number /* key */,
number /* index */,
number /* segment id */,
];

Expand Down Expand Up @@ -220,6 +219,7 @@ type SuspenseBoundary = {

export type Task = {
node: ReactNodeList,
childIndex: number,
ping: () => void,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment, // the segment we'll write to
Expand Down Expand Up @@ -1632,6 +1632,7 @@ function renderNodeDestructiveImpl(
// Stash the node we're working on. We'll pick up from this task in case
// something suspends.
task.node = node;
task.childIndex = childIndex;

// Handle object types
if (typeof node === 'object' && node !== null) {
Expand Down Expand Up @@ -1809,18 +1810,45 @@ function renderChildrenArray(
for (let i = 0; i < totalChildren; i++) {
const node = children[i];
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
if (isArray(node) || getIteratorFn(node)) {
// Nested arrays behave like a "fragment node" which is keyed.
// Therefore we need to add the current index as a parent key.

// Nested arrays behave like a "fragment node" which is keyed.
// Therefore we need to add the current index as a parent key.
// We first check if the nested nodes are arrays or iterables.

if (isArray(node)) {
const prevKeyPath = task.keyPath;
task.keyPath = [task.keyPath, '', childIndex];
renderNode(request, task, node, i);
renderChildrenArray(request, task, node, i);
task.keyPath = prevKeyPath;
} else {
// We need to use the non-destructive form so that we can safely pop back
// up and render the sibling if something suspends.
renderNode(request, task, node, i);
continue;
}

const iteratorFn = getIteratorFn(node);
if (iteratorFn) {
if (__DEV__) {
validateIterable(node, iteratorFn);
}
const iterator = iteratorFn.call(node);
if (iterator) {
let step = iterator.next();
if (!step.done) {
const prevKeyPath = task.keyPath;
task.keyPath = [task.keyPath, '', childIndex];
const nestedChildren = [];
do {
nestedChildren.push(step.value);
step = iterator.next();
} while (!step.done);
renderChildrenArray(request, task, nestedChildren, i);
task.keyPath = prevKeyPath;
}
continue;
}
}

// We need to use the non-destructive form so that we can safely pop back
// up and render the sibling if something suspends.
renderNode(request, task, node, i);
}
// Because this context is always set right before rendering every child, we
// only need to reset it to the previous value at the very end.
Expand All @@ -1831,6 +1859,7 @@ function trackPostpone(
request: Request,
trackedPostpones: PostponedHoles,
task: Task,
childIndex: number,
segment: Segment,
): void {
segment.status = POSTPONED;
Expand Down Expand Up @@ -1862,7 +1891,7 @@ function trackPostpone(
boundary.id,
];
trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
addToResumableParent(boundaryNode, boundaryKeyPath, trackedPostpones);
addToResumableParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
}

const keyPath = task.keyPath;
Expand All @@ -1872,12 +1901,7 @@ function trackPostpone(
);
}

const segmentNode: ResumableNode = [
RESUME_SEGMENT,
keyPath[1],
keyPath[2],
segment.id,
];
const segmentNode: ResumableNode = [RESUME_SEGMENT, childIndex, segment.id];
addToResumableParent(segmentNode, keyPath, trackedPostpones);
}

Expand Down Expand Up @@ -1941,6 +1965,7 @@ function spawnNewSuspendedTask(
task.context,
task.treeContext,
);
newTask.childIndex = task.childIndex;

if (__DEV__) {
if (task.componentStack !== null) {
Expand Down Expand Up @@ -2035,7 +2060,13 @@ function renderNode(
task,
postponeInstance.message,
);
trackPostpone(request, trackedPostpones, task, postponedSegment);
trackPostpone(
request,
trackedPostpones,
task,
childIndex,
postponedSegment,
);

// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
Expand Down Expand Up @@ -2328,7 +2359,13 @@ function retryTask(request: Request, task: Task): void {
const prevThenableState = task.thenableState;
task.thenableState = null;

renderNodeDestructive(request, task, prevThenableState, task.node, 0);
renderNodeDestructive(
request,
task,
prevThenableState,
task.node,
task.childIndex,
);
pushSegmentFinale(
segment.chunks,
request.renderState,
Expand Down Expand Up @@ -2377,8 +2414,15 @@ function retryTask(request: Request, task: Task): void {
task.abortSet.delete(task);
const postponeInstance: Postpone = (x: any);
logPostpone(request, postponeInstance.message);
trackPostpone(request, trackedPostpones, task, segment);
trackPostpone(
request,
trackedPostpones,
task,
task.childIndex,
segment,
);
finishedTask(request, task.blockedBoundary, segment);
return;
}
}
task.abortSet.delete(task);
Expand Down Expand Up @@ -2975,10 +3019,9 @@ export function getResumableState(request: Request): ResumableState {

function addToResumableParent(
node: ResumableNode,
keyPath: KeyNode,
parentKeyPath: Root | KeyNode,
trackedPostpones: PostponedHoles,
): void {
const parentKeyPath = keyPath[0];
if (parentKeyPath === null) {
trackedPostpones.root.push(node);
} else {
Expand All @@ -2992,7 +3035,7 @@ function addToResumableParent(
([]: Array<ResumableNode>),
]: ResumableParentNode);
workingMap.set(parentKeyPath, parentNode);
addToResumableParent(parentNode, parentKeyPath, trackedPostpones);
addToResumableParent(parentNode, parentKeyPath[0], trackedPostpones);
}
parentNode[3].push(node);
}
Expand Down

0 comments on commit 80063a5

Please sign in to comment.