[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 3710c4d4f9
commit f4041aa388
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 90 additions and 0 deletions

View File

@ -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>,
);
});
});

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) {