Repository: facebook/react
Pull request: [Fizz] Unblock SuspenseList when prerendering

There's an interesting case when a SuspenseList is partially prerendered but some of the completed boundaries are blocked by rows to be resumed.

This handles it but just unblocking the future rows to avoid stalling.

However, the correct semantics will need special handling in the postponed state.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
patch: @@ -29,6 +29,7 @@ let ReactDOM;
 let ReactDOMFizzServer;
 let ReactDOMFizzStatic;
 let Suspense;
+let SuspenseList;
 let container;
 let Scheduler;
 let act;
@@ -50,6 +51,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
     ReactDOMFizzServer = require('react-dom/server.browser');
     ReactDOMFizzStatic = require('react-dom/static.browser');
     Suspense = React.Suspense;
+    SuspenseList = React.unstable_SuspenseList;
     container = document.createElement('div');
     document.body.appendChild(container);
   });
@@ -2242,4 +2244,85 @@ describe('ReactDOMFizzStaticBrowser', () => {
       </html>,
     );
   });
+
+  // @gate enableHalt && enableSuspenseList
+  it('can resume a partially prerendered SuspenseList', async () => {
+    const errors = [];
+
+    let resolveA;
+    const promiseA = new Promise(r => (resolveA = r));
+    let resolveB;
+    const promiseB = new Promise(r => (resolveB = r));
+
+    async function ComponentA() {
+      await promiseA;
+      return 'A';
+    }
+
+    async function ComponentB() {
+      await promiseB;
+      return 'B';
+    }
+
+    function App() {
+      return (
+        <div>
+          <SuspenseList revealOrder="forwards">
+            <Suspense fallback="Loading A">
+              <ComponentA />
+            </Suspense>
+            <Suspense fallback="Loading B">
+              <ComponentB />
+            </Suspense>
+            <Suspense fallback="Loading C">C</Suspense>
+          </SuspenseList>
+        </div>
+      );
+    }
+
+    const controller = new AbortController();
+    const pendingResult = serverAct(() =>
+      ReactDOMFizzStatic.prerender(<App />, {
+        signal: controller.signal,
+        onError(x) {
+          errors.push(x.message);
+        },
+      }),
+    );
+
+    await serverAct(() => {
+      controller.abort();
+    });
+
+    const prerendered = await pendingResult;
+    const postponedState = JSON.stringify(prerendered.postponed);
+
+    await readIntoContainer(prerendered.prelude);
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        {'Loading A'}
+        {'Loading B'}
+        {'C' /* TODO: This should not be resolved. */}
+      </div>,
+    );
+
+    expect(prerendered.postponed).not.toBe(null);
+
+    await resolveA();
+    await resolveB();
+
+    const dynamic = await serverAct(() =>
+      ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
+    );
+
+    await readIntoContainer(dynamic);
+
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        {'A'}
+        {'B'}
+        {'C'}
+      </div>,
+    );
+  });
 });

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -4938,6 +4938,13 @@ function finishedTask(
           // preparation work during the work phase rather than the when flushing.
           preparePreamble(request);
         }
+      } else if (boundary.status === POSTPONED) {
+        const boundaryRow = boundary.row;
+        if (boundaryRow !== null) {
+          if (--boundaryRow.pendingTasks === 0) {
+            finishSuspenseListRow(request, boundaryRow);
+          }
+        }
       }
     } else {
       if (segment !== null && segment.parentFlushed) {