Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encode lazy Node slots properly in key path and resumable paths #27359

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('supports postponing in lazy in prerender and resuming later', async () => {
// @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