From dfff6a6c465b67070a7182338f557c15740fa051 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 20 May 2025 16:11:49 -0400 Subject: [PATCH 1/2] Unblock SuspenseList when prerendering --- .../ReactDOMFizzStaticBrowser-test.js | 81 +++++++++++++++++++ packages/react-server/src/ReactFizzServer.js | 7 ++ 2 files changed, 88 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 55a89bc525a3c..7e43b85b484c3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -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,83 @@ describe('ReactDOMFizzStaticBrowser', () => { , ); }); + + // @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 ( +
+ + + + + + + + C + +
+ ); + } + + const controller = new AbortController(); + let pendingResult; + await serverAct(async () => { + pendingResult = ReactDOMFizzStatic.prerender(, { + signal: controller.signal, + onError(x) { + errors.push(x.message); + }, + }); + }); + + controller.abort(); + const prerendered = await pendingResult; + const postponedState = JSON.stringify(prerendered.postponed); + + await readIntoContainer(prerendered.prelude); + expect(getVisibleChildren(container)).toEqual( +
+ {'Loading A'} + {'Loading B'} + {'C' /* TODO: This should not be resolved. */} +
, + ); + + expect(prerendered.postponed).not.toBe(null); + + await resolveA(); + await resolveB(); + + const dynamic = await serverAct(() => + ReactDOMFizzServer.resume(, JSON.parse(postponedState)), + ); + + await readIntoContainer(dynamic); + + expect(getVisibleChildren(container)).toEqual( +
+ {'A'} + {'B'} + {'C'} +
, + ); + }); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 1ea4568e1e3b1..f3c70b66c064d 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -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) { From e36c8f217f75e97fbbed3933a771be1898ff4538 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 21 May 2025 15:18:35 -0400 Subject: [PATCH 2/2] Use different pendingResult pattern The act of aborting changes the result and should be the thing that flushes. --- .../src/__tests__/ReactDOMFizzStaticBrowser-test.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 7e43b85b484c3..3d82300e8468e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -2281,17 +2281,19 @@ describe('ReactDOMFizzStaticBrowser', () => { } const controller = new AbortController(); - let pendingResult; - await serverAct(async () => { - pendingResult = ReactDOMFizzStatic.prerender(, { + const pendingResult = serverAct(() => + ReactDOMFizzStatic.prerender(, { signal: controller.signal, onError(x) { errors.push(x.message); }, - }); + }), + ); + + await serverAct(() => { + controller.abort(); }); - controller.abort(); const prerendered = await pendingResult; const postponedState = JSON.stringify(prerendered.postponed);