Skip to content

Commit

Permalink
Encode lazy Node slots properly in key path and resumable paths
Browse files Browse the repository at this point in the history
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 committed Sep 11, 2023
1 parent 627b7ab commit 1f03442
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 15 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
46 changes: 31 additions & 15 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 @@ -1831,6 +1832,7 @@ function trackPostpone(
request: Request,
trackedPostpones: PostponedHoles,
task: Task,
childIndex: number,
segment: Segment,
): void {
segment.status = POSTPONED;
Expand Down Expand Up @@ -1862,7 +1864,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 +1874,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 +1938,7 @@ function spawnNewSuspendedTask(
task.context,
task.treeContext,
);
newTask.childIndex = task.childIndex;

if (__DEV__) {
if (task.componentStack !== null) {
Expand Down Expand Up @@ -2035,7 +2033,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 +2332,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 +2387,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 +2992,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 +3008,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 1f03442

Please sign in to comment.