Repository: facebook/react
Pull request: [Fizz] Handle nested SuspenseList

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.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
patch: @@ -324,4 +324,77 @@ describe('ReactDOMFizSuspenseList', () => {
       </div>,
     );
   });
+
+  // @gate enableSuspenseList
+  it('waits for a nested SuspenseList to complete before resolving "forwards"', async () => {
+    const A = createAsyncText('A');
+    const B = createAsyncText('B');
+    const C = createAsyncText('C');
+
+    function Foo() {
+      return (
+        <div>
+          <SuspenseList revealOrder="forwards">
+            <SuspenseList revealOrder="backwards">
+              <Suspense fallback={<Text text="Loading A" />}>
+                <A />
+              </Suspense>
+              <Suspense fallback={<Text text="Loading B" />}>
+                <B />
+              </Suspense>
+            </SuspenseList>
+            <Suspense fallback={<Text text="Loading C" />}>
+              <C />
+            </Suspense>
+          </SuspenseList>
+        </div>
+      );
+    }
+
+    await C.resolve();
+
+    await serverAct(async () => {
+      const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
+      pipe(writable);
+    });
+
+    assertLog([
+      'Suspend! [B]',
+      'Suspend! [A]',
+      'C',
+      'Loading B',
+      'Loading A',
+      'Loading C',
+    ]);
+
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <span>Loading A</span>
+        <span>Loading B</span>
+        <span>Loading C</span>
+      </div>,
+    );
+
+    await serverAct(() => A.resolve());
+    assertLog(['A']);
+
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <span>Loading A</span>
+        <span>Loading B</span>
+        <span>Loading C</span>
+      </div>,
+    );
+
+    await serverAct(() => B.resolve());
+    assertLog(['B']);
+
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <span>A</span>
+        <span>B</span>
+        <span>C</span>
+      </div>,
+    );
+  });
 });

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -1732,12 +1732,12 @@ function renderSuspenseListRows(
   const prevRow = task.row;
   const totalChildren = rows.length;
 
+  let previousSuspenseListRow: null | SuspenseListRow = null;
   if (task.replay !== null) {
     // Replay
     // First we need to check if we have any resume slots at this level.
     const resumeSlots = task.replay.slots;
     if (resumeSlots !== null && typeof resumeSlots === 'object') {
-      let previousSuspenseListRow: null | SuspenseListRow = null;
       for (let n = 0; n < totalChildren; n++) {
         // Since we are going to resume into a slot whose order was already
         // determined by the prerender, we can safely resume it even in reverse
@@ -1763,7 +1763,6 @@ function renderSuspenseListRows(
         }
       }
     } else {
-      let previousSuspenseListRow: null | SuspenseListRow = null;
       for (let n = 0; n < totalChildren; n++) {
         // Since we are going to resume into a slot whose order was already
         // determined by the prerender, we can safely resume it even in reverse
@@ -1787,7 +1786,6 @@ function renderSuspenseListRows(
     task = ((task: any): RenderTask); // Refined
     if (revealOrder !== 'backwards') {
       // Forwards direction
-      let previousSuspenseListRow: null | SuspenseListRow = null;
       for (let i = 0; i < totalChildren; i++) {
         const node = rows[i];
         if (__DEV__) {
@@ -1809,7 +1807,6 @@ function renderSuspenseListRows(
       const parentSegment = task.blockedSegment;
       const childIndex = parentSegment.children.length;
       const insertionIndex = parentSegment.chunks.length;
-      let previousSuspenseListRow: null | SuspenseListRow = null;
       for (let i = totalChildren - 1; i >= 0; i--) {
         const node = rows[i];
         task.row = previousSuspenseListRow = createSuspenseListRow(
@@ -1859,6 +1856,17 @@ function renderSuspenseListRows(
     }
   }
 
+  if (
+    prevRow !== null &&
+    previousSuspenseListRow !== null &&
+    previousSuspenseListRow.pendingTasks > 0
+  ) {
+    // If we are part of an outer SuspenseList and our last row is still pending, then that blocks
+    // the parent row from completing. We can continue the chain.
+    prevRow.pendingTasks++;
+    previousSuspenseListRow.next = prevRow;
+  }
+
   // 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.
   task.treeContext = prevTreeContext;