Skip to content

Commit f4041aa

Browse files
authored
[Fizz] Unblock SuspenseList when prerendering (facebook#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.
1 parent 3710c4d commit f4041aa

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ let ReactDOM;
2929
let ReactDOMFizzServer;
3030
let ReactDOMFizzStatic;
3131
let Suspense;
32+
let SuspenseList;
3233
let container;
3334
let Scheduler;
3435
let act;
@@ -50,6 +51,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
5051
ReactDOMFizzServer = require('react-dom/server.browser');
5152
ReactDOMFizzStatic = require('react-dom/static.browser');
5253
Suspense = React.Suspense;
54+
SuspenseList = React.unstable_SuspenseList;
5355
container = document.createElement('div');
5456
document.body.appendChild(container);
5557
});
@@ -2242,4 +2244,85 @@ describe('ReactDOMFizzStaticBrowser', () => {
22422244
</html>,
22432245
);
22442246
});
2247+
2248+
// @gate enableHalt && enableSuspenseList
2249+
it('can resume a partially prerendered SuspenseList', async () => {
2250+
const errors = [];
2251+
2252+
let resolveA;
2253+
const promiseA = new Promise(r => (resolveA = r));
2254+
let resolveB;
2255+
const promiseB = new Promise(r => (resolveB = r));
2256+
2257+
async function ComponentA() {
2258+
await promiseA;
2259+
return 'A';
2260+
}
2261+
2262+
async function ComponentB() {
2263+
await promiseB;
2264+
return 'B';
2265+
}
2266+
2267+
function App() {
2268+
return (
2269+
<div>
2270+
<SuspenseList revealOrder="forwards">
2271+
<Suspense fallback="Loading A">
2272+
<ComponentA />
2273+
</Suspense>
2274+
<Suspense fallback="Loading B">
2275+
<ComponentB />
2276+
</Suspense>
2277+
<Suspense fallback="Loading C">C</Suspense>
2278+
</SuspenseList>
2279+
</div>
2280+
);
2281+
}
2282+
2283+
const controller = new AbortController();
2284+
const pendingResult = serverAct(() =>
2285+
ReactDOMFizzStatic.prerender(<App />, {
2286+
signal: controller.signal,
2287+
onError(x) {
2288+
errors.push(x.message);
2289+
},
2290+
}),
2291+
);
2292+
2293+
await serverAct(() => {
2294+
controller.abort();
2295+
});
2296+
2297+
const prerendered = await pendingResult;
2298+
const postponedState = JSON.stringify(prerendered.postponed);
2299+
2300+
await readIntoContainer(prerendered.prelude);
2301+
expect(getVisibleChildren(container)).toEqual(
2302+
<div>
2303+
{'Loading A'}
2304+
{'Loading B'}
2305+
{'C' /* TODO: This should not be resolved. */}
2306+
</div>,
2307+
);
2308+
2309+
expect(prerendered.postponed).not.toBe(null);
2310+
2311+
await resolveA();
2312+
await resolveB();
2313+
2314+
const dynamic = await serverAct(() =>
2315+
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
2316+
);
2317+
2318+
await readIntoContainer(dynamic);
2319+
2320+
expect(getVisibleChildren(container)).toEqual(
2321+
<div>
2322+
{'A'}
2323+
{'B'}
2324+
{'C'}
2325+
</div>,
2326+
);
2327+
});
22452328
});

packages/react-server/src/ReactFizzServer.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4938,6 +4938,13 @@ function finishedTask(
49384938
// preparation work during the work phase rather than the when flushing.
49394939
preparePreamble(request);
49404940
}
4941+
} else if (boundary.status === POSTPONED) {
4942+
const boundaryRow = boundary.row;
4943+
if (boundaryRow !== null) {
4944+
if (--boundaryRow.pendingTasks === 0) {
4945+
finishSuspenseListRow(request, boundaryRow);
4946+
}
4947+
}
49414948
}
49424949
} else {
49434950
if (segment !== null && segment.parentFlushed) {

0 commit comments

Comments
 (0)