Skip to content

Commit 040fe3e

Browse files
committed
Support "together" mode
1 parent 54e2d36 commit 040fe3e

File tree

2 files changed

+328
-2
lines changed

2 files changed

+328
-2
lines changed

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

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,290 @@ describe('ReactDOMFizSuspenseList', () => {
183183
);
184184
});
185185

186+
// @gate enableSuspenseList
187+
it('displays all "together"', async () => {
188+
const A = createAsyncText('A');
189+
const B = createAsyncText('B');
190+
const C = createAsyncText('C');
191+
192+
function Foo() {
193+
return (
194+
<div>
195+
<SuspenseList revealOrder="together">
196+
<Suspense fallback={<Text text="Loading A" />}>
197+
<A />
198+
</Suspense>
199+
<Suspense fallback={<Text text="Loading B" />}>
200+
<B />
201+
</Suspense>
202+
<Suspense fallback={<Text text="Loading C" />}>
203+
<C />
204+
</Suspense>
205+
</SuspenseList>
206+
</div>
207+
);
208+
}
209+
210+
await A.resolve();
211+
212+
await serverAct(async () => {
213+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
214+
pipe(writable);
215+
});
216+
217+
assertLog([
218+
'A',
219+
'Suspend! [B]',
220+
'Suspend! [C]',
221+
'Loading A',
222+
'Loading B',
223+
'Loading C',
224+
]);
225+
226+
expect(getVisibleChildren(container)).toEqual(
227+
<div>
228+
<span>Loading A</span>
229+
<span>Loading B</span>
230+
<span>Loading C</span>
231+
</div>,
232+
);
233+
234+
await serverAct(() => B.resolve());
235+
assertLog(['B']);
236+
237+
expect(getVisibleChildren(container)).toEqual(
238+
<div>
239+
<span>Loading A</span>
240+
<span>Loading B</span>
241+
<span>Loading C</span>
242+
</div>,
243+
);
244+
245+
await serverAct(() => C.resolve());
246+
assertLog(['C']);
247+
248+
expect(getVisibleChildren(container)).toEqual(
249+
<div>
250+
<span>A</span>
251+
<span>B</span>
252+
<span>C</span>
253+
</div>,
254+
);
255+
});
256+
257+
// @gate enableSuspenseList
258+
it('displays all "together" even when nested as siblings', async () => {
259+
const A = createAsyncText('A');
260+
const B = createAsyncText('B');
261+
const C = createAsyncText('C');
262+
263+
function Foo() {
264+
return (
265+
<div>
266+
<SuspenseList revealOrder="together">
267+
<div>
268+
<Suspense fallback={<Text text="Loading A" />}>
269+
<A />
270+
</Suspense>
271+
<Suspense fallback={<Text text="Loading B" />}>
272+
<B />
273+
</Suspense>
274+
</div>
275+
<div>
276+
<Suspense fallback={<Text text="Loading C" />}>
277+
<C />
278+
</Suspense>
279+
</div>
280+
</SuspenseList>
281+
</div>
282+
);
283+
}
284+
285+
await A.resolve();
286+
287+
await serverAct(async () => {
288+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
289+
pipe(writable);
290+
});
291+
292+
assertLog([
293+
'A',
294+
'Suspend! [B]',
295+
'Suspend! [C]',
296+
'Loading A',
297+
'Loading B',
298+
'Loading C',
299+
]);
300+
301+
expect(getVisibleChildren(container)).toEqual(
302+
<div>
303+
<div>
304+
<span>Loading A</span>
305+
<span>Loading B</span>
306+
</div>
307+
<div>
308+
<span>Loading C</span>
309+
</div>
310+
</div>,
311+
);
312+
313+
await serverAct(() => B.resolve());
314+
assertLog(['B']);
315+
316+
expect(getVisibleChildren(container)).toEqual(
317+
<div>
318+
<div>
319+
<span>Loading A</span>
320+
<span>Loading B</span>
321+
</div>
322+
<div>
323+
<span>Loading C</span>
324+
</div>
325+
</div>,
326+
);
327+
328+
await serverAct(() => C.resolve());
329+
assertLog(['C']);
330+
331+
expect(getVisibleChildren(container)).toEqual(
332+
<div>
333+
<div>
334+
<span>A</span>
335+
<span>B</span>
336+
</div>
337+
<div>
338+
<span>C</span>
339+
</div>
340+
</div>,
341+
);
342+
});
343+
344+
// @gate enableSuspenseList
345+
it('displays all "together" in nested SuspenseLists', async () => {
346+
const A = createAsyncText('A');
347+
const B = createAsyncText('B');
348+
const C = createAsyncText('C');
349+
350+
function Foo() {
351+
return (
352+
<div>
353+
<SuspenseList revealOrder="together">
354+
<Suspense fallback={<Text text="Loading A" />}>
355+
<A />
356+
</Suspense>
357+
<SuspenseList revealOrder="together">
358+
<Suspense fallback={<Text text="Loading B" />}>
359+
<B />
360+
</Suspense>
361+
<Suspense fallback={<Text text="Loading C" />}>
362+
<C />
363+
</Suspense>
364+
</SuspenseList>
365+
</SuspenseList>
366+
</div>
367+
);
368+
}
369+
370+
await A.resolve();
371+
await B.resolve();
372+
373+
await serverAct(async () => {
374+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
375+
pipe(writable);
376+
});
377+
378+
assertLog([
379+
'A',
380+
'B',
381+
'Suspend! [C]',
382+
'Loading A',
383+
'Loading B',
384+
'Loading C',
385+
]);
386+
387+
expect(getVisibleChildren(container)).toEqual(
388+
<div>
389+
<span>Loading A</span>
390+
<span>Loading B</span>
391+
<span>Loading C</span>
392+
</div>,
393+
);
394+
395+
await serverAct(() => C.resolve());
396+
assertLog(['C']);
397+
398+
expect(getVisibleChildren(container)).toEqual(
399+
<div>
400+
<span>A</span>
401+
<span>B</span>
402+
<span>C</span>
403+
</div>,
404+
);
405+
});
406+
407+
// @gate enableSuspenseList
408+
it('displays all "together" in nested SuspenseLists where the inner is default', async () => {
409+
const A = createAsyncText('A');
410+
const B = createAsyncText('B');
411+
const C = createAsyncText('C');
412+
413+
function Foo() {
414+
return (
415+
<div>
416+
<SuspenseList revealOrder="together">
417+
<Suspense fallback={<Text text="Loading A" />}>
418+
<A />
419+
</Suspense>
420+
<SuspenseList>
421+
<Suspense fallback={<Text text="Loading B" />}>
422+
<B />
423+
</Suspense>
424+
<Suspense fallback={<Text text="Loading C" />}>
425+
<C />
426+
</Suspense>
427+
</SuspenseList>
428+
</SuspenseList>
429+
</div>
430+
);
431+
}
432+
433+
await A.resolve();
434+
await B.resolve();
435+
436+
await serverAct(async () => {
437+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
438+
pipe(writable);
439+
});
440+
441+
assertLog([
442+
'A',
443+
'B',
444+
'Suspend! [C]',
445+
'Loading A',
446+
'Loading B',
447+
'Loading C',
448+
]);
449+
450+
expect(getVisibleChildren(container)).toEqual(
451+
<div>
452+
<span>Loading A</span>
453+
<span>Loading B</span>
454+
<span>Loading C</span>
455+
</div>,
456+
);
457+
458+
await serverAct(() => C.resolve());
459+
assertLog(['C']);
460+
461+
expect(getVisibleChildren(container)).toEqual(
462+
<div>
463+
<span>A</span>
464+
<span>B</span>
465+
<span>C</span>
466+
</div>,
467+
);
468+
});
469+
186470
// @gate enableSuspenseList
187471
it('displays each items in "forwards" order', async () => {
188472
const A = createAsyncText('A');

packages/react-server/src/ReactFizzServer.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ type LegacyContext = {
236236
type SuspenseListRow = {
237237
pendingTasks: number, // The number of tasks, previous rows and inner suspense boundaries blocking this row.
238238
boundaries: null | Array<SuspenseBoundary>, // The boundaries in this row waiting to be unblocked by the previous row. (null means this row is not blocked)
239+
together: boolean, // All the boundaries within this row must be revealed together.
239240
next: null | SuspenseListRow, // The next row blocked by this one.
240241
};
241242

@@ -1670,8 +1671,14 @@ function replaySuspenseBoundary(
16701671

16711672
function finishSuspenseListRow(request: Request, row: SuspenseListRow): void {
16721673
// This row finished. Now we have to unblock all the next rows that were blocked on this.
1674+
unblockSuspenseListRow(request, row.next);
1675+
}
1676+
1677+
function unblockSuspenseListRow(
1678+
request: Request,
1679+
unblockedRow: null | SuspenseListRow,
1680+
): void {
16731681
// We do this in a loop to avoid stack overflow for very long lists that get unblocked.
1674-
let unblockedRow = row.next;
16751682
while (unblockedRow !== null) {
16761683
// Unblocking the boundaries will decrement the count of this row but we keep it above
16771684
// zero so they never finish this row recursively.
@@ -1698,6 +1705,7 @@ function createSuspenseListRow(
16981705
const newRow: SuspenseListRow = {
16991706
pendingTasks: 1, // At first the row is blocked on attempting rendering itself.
17001707
boundaries: null,
1708+
together: false,
17011709
next: null,
17021710
};
17031711
if (previousRow !== null && previousRow.pendingTasks > 0) {
@@ -1978,7 +1986,27 @@ function renderSuspenseList(
19781986
}
19791987

19801988
if (revealOrder === 'together') {
1981-
// TODO
1989+
const prevKeyPath = task.keyPath;
1990+
const prevRow = task.row;
1991+
const newRow = (task.row = createSuspenseListRow(null));
1992+
// This will cause boundaries to block on this row, but there's nothing to
1993+
// unblock them. We'll use the partial flushing pass to unblock them.
1994+
newRow.boundaries = [];
1995+
newRow.together = true;
1996+
task.keyPath = keyPath;
1997+
renderNodeDestructive(request, task, children, -1);
1998+
if (--newRow.pendingTasks === 0) {
1999+
finishSuspenseListRow(request, newRow);
2000+
}
2001+
task.keyPath = prevKeyPath;
2002+
task.row = prevRow;
2003+
if (prevRow !== null && newRow.pendingTasks > 0) {
2004+
// If we are part of an outer SuspenseList and our row is still pending, then that blocks
2005+
// the parent row from completing. We can continue the chain.
2006+
prevRow.pendingTasks++;
2007+
newRow.next = prevRow;
2008+
}
2009+
return;
19822010
}
19832011
// For other reveal order modes, we just render it as a fragment.
19842012
const prevKeyPath = task.keyPath;
@@ -5604,6 +5632,20 @@ function flushPartialBoundary(
56045632
}
56055633
completedSegments.splice(0, i);
56065634

5635+
const row = boundary.row;
5636+
if (row !== null && row.together && boundary.pendingTasks === 1) {
5637+
// "together" rows are blocked on their own boundaries.
5638+
// We have now flushed all the boundary's segments as partials.
5639+
// We can now unblock it from blocking the row that will eventually
5640+
// unblock the boundary itself which can issue its complete instruction.
5641+
// TODO: Ideally the complete instruction would be in a single <script> tag.
5642+
if (row.pendingTasks === 1) {
5643+
unblockSuspenseListRow(request, row);
5644+
} else {
5645+
row.pendingTasks--;
5646+
}
5647+
}
5648+
56075649
return writeHoistablesForBoundary(
56085650
destination,
56095651
boundary.contentState,

0 commit comments

Comments
 (0)