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

[Fizz] Handle nested SuspenseList (#33308)

Follow up to #33306. If we're nested inside a SuspenseList and we have a row, then we can point our last row to block the parent row and unblock the parent when the last child unblocks.

Sebastian Markbåge committed May 20, 2025 at 09:39 UTC c4676e72a630f3e93634c2b004b3be07b17a79c8
2 files changed +85 -4
packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
+73
@@ -324,4 +324,77 @@ describe('ReactDOMFizSuspenseList', () => {
324 </div>,
325 );
326 });
327 +
328 + // @gate enableSuspenseList
329 + it('waits for a nested SuspenseList to complete before resolving "forwards"', async () => {
330 + const A = createAsyncText('A');
331 + const B = createAsyncText('B');
332 + const C = createAsyncText('C');
333 +
334 + function Foo() {
335 + return (
336 + <div>
337 + <SuspenseList revealOrder="forwards">
338 + <SuspenseList revealOrder="backwards">
339 + <Suspense fallback={<Text text="Loading A" />}>
340 + <A />
341 + </Suspense>
342 + <Suspense fallback={<Text text="Loading B" />}>
343 + <B />
344 + </Suspense>
345 + </SuspenseList>
346 + <Suspense fallback={<Text text="Loading C" />}>
347 + <C />
348 + </Suspense>
349 + </SuspenseList>
350 + </div>
351 + );
352 + }
353 +
354 + await C.resolve();
355 +
356 + await serverAct(async () => {
357 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
358 + pipe(writable);
359 + });
360 +
361 + assertLog([
362 + 'Suspend! [B]',
363 + 'Suspend! [A]',
364 + 'C',
365 + 'Loading B',
366 + 'Loading A',
367 + 'Loading C',
368 + ]);
369 +
370 + expect(getVisibleChildren(container)).toEqual(
371 + <div>
372 + <span>Loading A</span>
373 + <span>Loading B</span>
374 + <span>Loading C</span>
375 + </div>,
376 + );
377 +
378 + await serverAct(() => A.resolve());
379 + assertLog(['A']);
380 +
381 + expect(getVisibleChildren(container)).toEqual(
382 + <div>
383 + <span>Loading A</span>
384 + <span>Loading B</span>
385 + <span>Loading C</span>
386 + </div>,
387 + );
388 +
389 + await serverAct(() => B.resolve());
390 + assertLog(['B']);
391 +
392 + expect(getVisibleChildren(container)).toEqual(
393 + <div>
394 + <span>A</span>
395 + <span>B</span>
396 + <span>C</span>
397 + </div>,
398 + );
399 + });
400 });
packages/react-server/src/ReactFizzServer.js
+12 -4
@@ -1732,12 +1732,12 @@ function renderSuspenseListRows(
1732 const prevRow = task.row;
1733 const totalChildren = rows.length;
1734
1735 + let previousSuspenseListRow: null | SuspenseListRow = null;
1736 if (task.replay !== null) {
1737 // Replay
1738 // First we need to check if we have any resume slots at this level.
1739 const resumeSlots = task.replay.slots;
1740 if (resumeSlots !== null && typeof resumeSlots === 'object') {
1740 - let previousSuspenseListRow: null | SuspenseListRow = null;
1741 for (let n = 0; n < totalChildren; n++) {
1742 // Since we are going to resume into a slot whose order was already
1743 // determined by the prerender, we can safely resume it even in reverse
@@ -1763,7 +1763,6 @@ function renderSuspenseListRows(
1763 }
1764 }
1765 } else {
1766 - let previousSuspenseListRow: null | SuspenseListRow = null;
1766 for (let n = 0; n < totalChildren; n++) {
1767 // Since we are going to resume into a slot whose order was already
1768 // determined by the prerender, we can safely resume it even in reverse
@@ -1787,7 +1786,6 @@ function renderSuspenseListRows(
1786 task = ((task: any): RenderTask); // Refined
1787 if (revealOrder !== 'backwards') {
1788 // Forwards direction
1790 - let previousSuspenseListRow: null | SuspenseListRow = null;
1789 for (let i = 0; i < totalChildren; i++) {
1790 const node = rows[i];
1791 if (__DEV__) {
@@ -1809,7 +1807,6 @@ function renderSuspenseListRows(
1807 const parentSegment = task.blockedSegment;
1808 const childIndex = parentSegment.children.length;
1809 const insertionIndex = parentSegment.chunks.length;
1812 - let previousSuspenseListRow: null | SuspenseListRow = null;
1810 for (let i = totalChildren - 1; i >= 0; i--) {
1811 const node = rows[i];
1812 task.row = previousSuspenseListRow = createSuspenseListRow(
@@ -1859,6 +1856,17 @@ function renderSuspenseListRows(
1856 }
1857 }
1858
1859 + if (
1860 + prevRow !== null &&
1861 + previousSuspenseListRow !== null &&
1862 + previousSuspenseListRow.pendingTasks > 0
1863 + ) {
1864 + // If we are part of an outer SuspenseList and our last row is still pending, then that blocks
1865 + // the parent row from completing. We can continue the chain.
1866 + prevRow.pendingTasks++;
1867 + previousSuspenseListRow.next = prevRow;
1868 + }
1869 +
1870 // Because this context is always set right before rendering every child, we
1871 // only need to reset it to the previous value at the very end.
1872 task.treeContext = prevTreeContext;