Skip to content

Commit bb1d8d1

Browse files
authored
Encode lazy Node slots properly in key path and resumable paths (#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.
1 parent a4aceaf commit bb1d8d1

File tree

2 files changed

+107
-23
lines changed

2 files changed

+107
-23
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,47 @@ describe('ReactDOMFizzStaticBrowser', () => {
493493
// TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
494494
});
495495

496+
// @gate enablePostpone
497+
it('supports postponing in lazy in prerender and resuming later', async () => {
498+
let prerendering = true;
499+
const Hole = React.lazy(async () => {
500+
React.unstable_postpone();
501+
});
502+
503+
function Postpone() {
504+
return 'Hello';
505+
}
506+
507+
function App() {
508+
return (
509+
<div>
510+
<Suspense fallback="Loading...">
511+
Hi
512+
{prerendering ? Hole : <Postpone />}
513+
</Suspense>
514+
</div>
515+
);
516+
}
517+
518+
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
519+
expect(prerendered.postponed).not.toBe(null);
520+
521+
prerendering = false;
522+
523+
const resumed = await ReactDOMFizzServer.resume(
524+
<App />,
525+
prerendered.postponed,
526+
);
527+
528+
await readIntoContainer(prerendered.prelude);
529+
530+
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
531+
532+
await readIntoContainer(resumed);
533+
534+
// TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
535+
});
536+
496537
// @gate enablePostpone
497538
it('only emits end tags once when resuming', async () => {
498539
let prerendering = true;

packages/react-server/src/ReactFizzServer.js

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,7 @@ type ResumableNode =
186186
| ResumableParentNode
187187
| [
188188
2, // RESUME_SEGMENT
189-
string | null /* name */,
190-
string | number /* key */,
189+
number /* index */,
191190
number /* segment id */,
192191
];
193192

@@ -220,6 +219,7 @@ type SuspenseBoundary = {
220219

221220
export type Task = {
222221
node: ReactNodeList,
222+
childIndex: number,
223223
ping: () => void,
224224
blockedBoundary: Root | SuspenseBoundary,
225225
blockedSegment: Segment, // the segment we'll write to
@@ -1632,6 +1632,7 @@ function renderNodeDestructiveImpl(
16321632
// Stash the node we're working on. We'll pick up from this task in case
16331633
// something suspends.
16341634
task.node = node;
1635+
task.childIndex = childIndex;
16351636

16361637
// Handle object types
16371638
if (typeof node === 'object' && node !== null) {
@@ -1809,18 +1810,45 @@ function renderChildrenArray(
18091810
for (let i = 0; i < totalChildren; i++) {
18101811
const node = children[i];
18111812
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
1812-
if (isArray(node) || getIteratorFn(node)) {
1813-
// Nested arrays behave like a "fragment node" which is keyed.
1814-
// Therefore we need to add the current index as a parent key.
1813+
1814+
// Nested arrays behave like a "fragment node" which is keyed.
1815+
// Therefore we need to add the current index as a parent key.
1816+
// We first check if the nested nodes are arrays or iterables.
1817+
1818+
if (isArray(node)) {
18151819
const prevKeyPath = task.keyPath;
18161820
task.keyPath = [task.keyPath, '', childIndex];
1817-
renderNode(request, task, node, i);
1821+
renderChildrenArray(request, task, node, i);
18181822
task.keyPath = prevKeyPath;
1819-
} else {
1820-
// We need to use the non-destructive form so that we can safely pop back
1821-
// up and render the sibling if something suspends.
1822-
renderNode(request, task, node, i);
1823+
continue;
18231824
}
1825+
1826+
const iteratorFn = getIteratorFn(node);
1827+
if (iteratorFn) {
1828+
if (__DEV__) {
1829+
validateIterable(node, iteratorFn);
1830+
}
1831+
const iterator = iteratorFn.call(node);
1832+
if (iterator) {
1833+
let step = iterator.next();
1834+
if (!step.done) {
1835+
const prevKeyPath = task.keyPath;
1836+
task.keyPath = [task.keyPath, '', childIndex];
1837+
const nestedChildren = [];
1838+
do {
1839+
nestedChildren.push(step.value);
1840+
step = iterator.next();
1841+
} while (!step.done);
1842+
renderChildrenArray(request, task, nestedChildren, i);
1843+
task.keyPath = prevKeyPath;
1844+
}
1845+
continue;
1846+
}
1847+
}
1848+
1849+
// We need to use the non-destructive form so that we can safely pop back
1850+
// up and render the sibling if something suspends.
1851+
renderNode(request, task, node, i);
18241852
}
18251853
// Because this context is always set right before rendering every child, we
18261854
// only need to reset it to the previous value at the very end.
@@ -1831,6 +1859,7 @@ function trackPostpone(
18311859
request: Request,
18321860
trackedPostpones: PostponedHoles,
18331861
task: Task,
1862+
childIndex: number,
18341863
segment: Segment,
18351864
): void {
18361865
segment.status = POSTPONED;
@@ -1862,7 +1891,7 @@ function trackPostpone(
18621891
boundary.id,
18631892
];
18641893
trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
1865-
addToResumableParent(boundaryNode, boundaryKeyPath, trackedPostpones);
1894+
addToResumableParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
18661895
}
18671896

18681897
const keyPath = task.keyPath;
@@ -1872,12 +1901,7 @@ function trackPostpone(
18721901
);
18731902
}
18741903

1875-
const segmentNode: ResumableNode = [
1876-
RESUME_SEGMENT,
1877-
keyPath[1],
1878-
keyPath[2],
1879-
segment.id,
1880-
];
1904+
const segmentNode: ResumableNode = [RESUME_SEGMENT, childIndex, segment.id];
18811905
addToResumableParent(segmentNode, keyPath, trackedPostpones);
18821906
}
18831907

@@ -1941,6 +1965,7 @@ function spawnNewSuspendedTask(
19411965
task.context,
19421966
task.treeContext,
19431967
);
1968+
newTask.childIndex = task.childIndex;
19441969

19451970
if (__DEV__) {
19461971
if (task.componentStack !== null) {
@@ -2035,7 +2060,13 @@ function renderNode(
20352060
task,
20362061
postponeInstance.message,
20372062
);
2038-
trackPostpone(request, trackedPostpones, task, postponedSegment);
2063+
trackPostpone(
2064+
request,
2065+
trackedPostpones,
2066+
task,
2067+
childIndex,
2068+
postponedSegment,
2069+
);
20392070

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

2331-
renderNodeDestructive(request, task, prevThenableState, task.node, 0);
2362+
renderNodeDestructive(
2363+
request,
2364+
task,
2365+
prevThenableState,
2366+
task.node,
2367+
task.childIndex,
2368+
);
23322369
pushSegmentFinale(
23332370
segment.chunks,
23342371
request.renderState,
@@ -2377,8 +2414,15 @@ function retryTask(request: Request, task: Task): void {
23772414
task.abortSet.delete(task);
23782415
const postponeInstance: Postpone = (x: any);
23792416
logPostpone(request, postponeInstance.message);
2380-
trackPostpone(request, trackedPostpones, task, segment);
2417+
trackPostpone(
2418+
request,
2419+
trackedPostpones,
2420+
task,
2421+
task.childIndex,
2422+
segment,
2423+
);
23812424
finishedTask(request, task.blockedBoundary, segment);
2425+
return;
23822426
}
23832427
}
23842428
task.abortSet.delete(task);
@@ -2975,10 +3019,9 @@ export function getResumableState(request: Request): ResumableState {
29753019

29763020
function addToResumableParent(
29773021
node: ResumableNode,
2978-
keyPath: KeyNode,
3022+
parentKeyPath: Root | KeyNode,
29793023
trackedPostpones: PostponedHoles,
29803024
): void {
2981-
const parentKeyPath = keyPath[0];
29823025
if (parentKeyPath === null) {
29833026
trackedPostpones.root.push(node);
29843027
} else {
@@ -2992,7 +3035,7 @@ function addToResumableParent(
29923035
([]: Array<ResumableNode>),
29933036
]: ResumableParentNode);
29943037
workingMap.set(parentKeyPath, parentNode);
2995-
addToResumableParent(parentNode, parentKeyPath, trackedPostpones);
3038+
addToResumableParent(parentNode, parentKeyPath[0], trackedPostpones);
29963039
}
29973040
parentNode[3].push(node);
29983041
}

0 commit comments

Comments
 (0)