@samitouri / QOS-React-2 / commits / bb1d8d1667

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 }