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.
Sebastian Markbåge committed
Sep 11, 2023 at 19:25 UTC
bb1d8d166799eb97892be6c7826179270ba283d0
2 files changed
+107
-23
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+41
@@ -493,6 +493,47 @@ describe('ReactDOMFizzStaticBrowser', () => {
493
// TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
494
});
495
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
+
537
// @gate enablePostpone
538
it('only emits end tags once when resuming', async () => {
539
let prerendering = true;
packages/react-server/src/ReactFizzServer.js
+66
-23
@@ -186,8 +186,7 @@ type ResumableNode =
186
| ResumableParentNode
187
| [
188
2, // RESUME_SEGMENT
189
- string | null /* name */,
190
- string | number /* key */,
189
+ number /* index */,
190
number /* segment id */,
191
];
192
@@ -220,6 +219,7 @@ type SuspenseBoundary = {
219
220
export type Task = {
221
node: ReactNodeList,
222
+ childIndex: number,
223
ping: () => void,
224
blockedBoundary: Root | SuspenseBoundary,
225
blockedSegment: Segment, // the segment we'll write to
@@ -1632,6 +1632,7 @@ function renderNodeDestructiveImpl(
1632
// Stash the node we're working on. We'll pick up from this task in case
1633
// something suspends.
1634
task.node = node;
1635
+ task.childIndex = childIndex;
1636
1637
// Handle object types
1638
if (typeof node === 'object' && node !== null) {
@@ -1809,18 +1810,45 @@ function renderChildrenArray(
1810
for (let i = 0; i < totalChildren; i++) {
1811
const node = children[i];
1812
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)) {
1819
const prevKeyPath = task.keyPath;
1820
task.keyPath = [task.keyPath, '', childIndex];
1817
- renderNode(request, task, node, i);
1821
+ renderChildrenArray(request, task, node, i);
1822
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;
1824
}
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);
1852
}
1853
// Because this context is always set right before rendering every child, we
1854
// only need to reset it to the previous value at the very end.
@@ -1831,6 +1859,7 @@ function trackPostpone(
1859
request: Request,
1860
trackedPostpones: PostponedHoles,
1861
task: Task,
1862
+ childIndex: number,
1863
segment: Segment,
1864
): void {
1865
segment.status = POSTPONED;
@@ -1862,7 +1891,7 @@ function trackPostpone(
1891
boundary.id,
1892
];
1893
trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
1865
- addToResumableParent(boundaryNode, boundaryKeyPath, trackedPostpones);
1894
+ addToResumableParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
1895
}
1896
1897
const keyPath = task.keyPath;
@@ -1872,12 +1901,7 @@ function trackPostpone(
1901
);
1902
}
1903
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];
1905
addToResumableParent(segmentNode, keyPath, trackedPostpones);
1906
}
1907
@@ -1941,6 +1965,7 @@ function spawnNewSuspendedTask(
1965
task.context,
1966
task.treeContext,
1967
);
1968
+ newTask.childIndex = task.childIndex;
1969
1970
if (__DEV__) {
1971
if (task.componentStack !== null) {
@@ -2035,7 +2060,13 @@ function renderNode(
2060
task,
2061
postponeInstance.message,
2062
);
2038
- trackPostpone(request, trackedPostpones, task, postponedSegment);
2063
+ trackPostpone(
2064
+ request,
2065
+ trackedPostpones,
2066
+ task,
2067
+ childIndex,
2068
+ postponedSegment,
2069
+ );
2070
2071
// Restore the context. We assume that this will be restored by the inner
2072
// functions in case nothing throws so we don't use "finally" here.
@@ -2328,7 +2359,13 @@ function retryTask(request: Request, task: Task): void {
2359
const prevThenableState = task.thenableState;
2360
task.thenableState = null;
2361
2331
- renderNodeDestructive(request, task, prevThenableState, task.node, 0);
2362
+ renderNodeDestructive(
2363
+ request,
2364
+ task,
2365
+ prevThenableState,
2366
+ task.node,
2367
+ task.childIndex,
2368
+ );
2369
pushSegmentFinale(
2370
segment.chunks,
2371
request.renderState,
@@ -2377,8 +2414,15 @@ function retryTask(request: Request, task: Task): void {
2414
task.abortSet.delete(task);
2415
const postponeInstance: Postpone = (x: any);
2416
logPostpone(request, postponeInstance.message);
2380
- trackPostpone(request, trackedPostpones, task, segment);
2417
+ trackPostpone(
2418
+ request,
2419
+ trackedPostpones,
2420
+ task,
2421
+ task.childIndex,
2422
+ segment,
2423
+ );
2424
finishedTask(request, task.blockedBoundary, segment);
2425
+ return;
2426
}
2427
}
2428
task.abortSet.delete(task);
@@ -2975,10 +3019,9 @@ export function getResumableState(request: Request): ResumableState {
3019
3020
function addToResumableParent(
3021
node: ResumableNode,
2978
- keyPath: KeyNode,
3022
+ parentKeyPath: Root | KeyNode,
3023
trackedPostpones: PostponedHoles,
3024
): void {
2981
- const parentKeyPath = keyPath[0];
3025
if (parentKeyPath === null) {
3026
trackedPostpones.root.push(node);
3027
} else {
@@ -2992,7 +3035,7 @@ function addToResumableParent(
3035
([]: Array<ResumableNode>),
3036
]: ResumableParentNode);
3037
workingMap.set(parentKeyPath, parentNode);
2995
- addToResumableParent(parentNode, parentKeyPath, trackedPostpones);
3038
+ addToResumableParent(parentNode, parentKeyPath[0], trackedPostpones);
3039
}
3040
parentNode[3].push(node);
3041
}