Skip to content

[Fizz] Handle nested SuspenseList #33308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,77 @@ describe('ReactDOMFizSuspenseList', () => {
</div>,
);
});

// @gate enableSuspenseList
it('waits for a nested SuspenseList to complete before resolving "forwards"', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
const C = createAsyncText('C');

function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="backwards">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
</SuspenseList>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
</div>
);
}

await C.resolve();

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});

assertLog([
'Suspend! [B]',
'Suspend! [A]',
'C',
'Loading B',
'Loading A',
'Loading C',
]);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);

await serverAct(() => A.resolve());
assertLog(['A']);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);

await serverAct(() => B.resolve());
assertLog(['B']);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B</span>
<span>C</span>
</div>,
);
});
});
16 changes: 12 additions & 4 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1732,12 +1732,12 @@ function renderSuspenseListRows(
const prevRow = task.row;
const totalChildren = rows.length;

let previousSuspenseListRow: null | SuspenseListRow = null;
if (task.replay !== null) {
// Replay
// First we need to check if we have any resume slots at this level.
const resumeSlots = task.replay.slots;
if (resumeSlots !== null && typeof resumeSlots === 'object') {
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let n = 0; n < totalChildren; n++) {
// Since we are going to resume into a slot whose order was already
// determined by the prerender, we can safely resume it even in reverse
Expand All @@ -1763,7 +1763,6 @@ function renderSuspenseListRows(
}
}
} else {
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let n = 0; n < totalChildren; n++) {
// Since we are going to resume into a slot whose order was already
// determined by the prerender, we can safely resume it even in reverse
Expand All @@ -1787,7 +1786,6 @@ function renderSuspenseListRows(
task = ((task: any): RenderTask); // Refined
if (revealOrder !== 'backwards') {
// Forwards direction
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let i = 0; i < totalChildren; i++) {
const node = rows[i];
if (__DEV__) {
Expand All @@ -1809,7 +1807,6 @@ function renderSuspenseListRows(
const parentSegment = task.blockedSegment;
const childIndex = parentSegment.children.length;
const insertionIndex = parentSegment.chunks.length;
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let i = totalChildren - 1; i >= 0; i--) {
const node = rows[i];
task.row = previousSuspenseListRow = createSuspenseListRow(
Expand Down Expand Up @@ -1859,6 +1856,17 @@ function renderSuspenseListRows(
}
}

if (
prevRow !== null &&
previousSuspenseListRow !== null &&
previousSuspenseListRow.pendingTasks > 0
) {
// If we are part of an outer SuspenseList and our last row is still pending, then that blocks
// the parent row from completing. We can continue the chain.
prevRow.pendingTasks++;
previousSuspenseListRow.next = prevRow;
}

// Because this context is always set right before rendering every child, we
// only need to reset it to the previous value at the very end.
task.treeContext = prevTreeContext;
Expand Down
Loading