[Fizz] Unblock SuspenseList when prerendering (#33321)

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.
This commit is contained in:
Sebastian Markbåge
2025-05-21 15:31:22 -04:00
committed by GitHub
parent dfeeacd523
commit 24172ecff4
2 changed files with 90 additions and 0 deletions
@@ -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>,
);
});
});
+7
View File
@@ -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) {