Skip to content

Commit 99aa685

Browse files
authored
[Fizz] Support SuspenseList revealOrder="together" (#33311)
Stacked on #33308. For "together" mode, we can be a self-blocking row that adds all its boundaries to the blocked set, but there's no parent row that unblocks it. A particular quirk of this mode is that it's not enough to just unblock them all on the server together. Because if one boundary downloads all its html and then issues a complete instruction it'll appear before the others while streaming in. What we actually want is to reveal them all in a single batch. This implementation takes a short cut by unblocking the rows in `flushPartialBoundary`. That ensures that all the segments of every boundary has a chance to flush before we start emitting any of the complete boundary instructions. Once the last one unblocks, all the complete boundary instructions are queued. Ideally this would be a single `<script>` tag so that they can't be split up even if we get a chunk containing some of them. ~A downside of this approach is that we always outline these boundaries. We could inline them if they all complete before the parent flushes. E.g. by checking if the row is blocked only by its own boundaries and if all the boundaries would fit without getting outlined, then we can inline them all at once.~ I went ahead and did this because it solves an issue with `renderToString` where it doesn't support the script runtime so it can only handle this if inlined.
1 parent d38c7e1 commit 99aa685

File tree

3 files changed

+404
-3
lines changed

3 files changed

+404
-3
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ export {
172172
completeResumableState,
173173
emitEarlyPreloads,
174174
supportsClientAPIs,
175-
canHavePreamble,
176175
hoistPreambleState,
177176
isPreambleReady,
178177
isPreambleContext,
@@ -194,6 +193,10 @@ export function getViewTransitionFormatContext(
194193
return parentContext;
195194
}
196195

196+
export function canHavePreamble(formatContext: FormatContext): boolean {
197+
return false;
198+
}
199+
197200
export function pushTextInstance(
198201
target: Array<Chunk | PrecomputedChunk>,
199202
text: string,

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

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,323 @@ 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" in a single pass', async () => {
259+
function Foo() {
260+
return (
261+
<div>
262+
<SuspenseList revealOrder="together">
263+
<Suspense fallback={<Text text="Loading A" />}>
264+
<Text text="A" />
265+
</Suspense>
266+
<Suspense fallback={<Text text="Loading B" />}>
267+
<Text text="B" />
268+
</Suspense>
269+
<Suspense fallback={<Text text="Loading C" />}>
270+
<Text text="C" />
271+
</Suspense>
272+
</SuspenseList>
273+
</div>
274+
);
275+
}
276+
277+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
278+
pipe(writable);
279+
await 0;
280+
const bufferedContent = buffer;
281+
buffer = '';
282+
283+
assertLog(['A', 'B', 'C', 'Loading A', 'Loading B', 'Loading C']);
284+
285+
expect(bufferedContent).toMatchInlineSnapshot(
286+
`"<div><!--$--><span>A</span><!--/$--><!--$--><span>B</span><!--/$--><!--$--><span>C</span><!--/$--></div>"`,
287+
);
288+
});
289+
290+
// @gate enableSuspenseList
291+
it('displays all "together" even when nested as siblings', async () => {
292+
const A = createAsyncText('A');
293+
const B = createAsyncText('B');
294+
const C = createAsyncText('C');
295+
296+
function Foo() {
297+
return (
298+
<div>
299+
<SuspenseList revealOrder="together">
300+
<div>
301+
<Suspense fallback={<Text text="Loading A" />}>
302+
<A />
303+
</Suspense>
304+
<Suspense fallback={<Text text="Loading B" />}>
305+
<B />
306+
</Suspense>
307+
</div>
308+
<div>
309+
<Suspense fallback={<Text text="Loading C" />}>
310+
<C />
311+
</Suspense>
312+
</div>
313+
</SuspenseList>
314+
</div>
315+
);
316+
}
317+
318+
await A.resolve();
319+
320+
await serverAct(async () => {
321+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
322+
pipe(writable);
323+
});
324+
325+
assertLog([
326+
'A',
327+
'Suspend! [B]',
328+
'Suspend! [C]',
329+
'Loading A',
330+
'Loading B',
331+
'Loading C',
332+
]);
333+
334+
expect(getVisibleChildren(container)).toEqual(
335+
<div>
336+
<div>
337+
<span>Loading A</span>
338+
<span>Loading B</span>
339+
</div>
340+
<div>
341+
<span>Loading C</span>
342+
</div>
343+
</div>,
344+
);
345+
346+
await serverAct(() => B.resolve());
347+
assertLog(['B']);
348+
349+
expect(getVisibleChildren(container)).toEqual(
350+
<div>
351+
<div>
352+
<span>Loading A</span>
353+
<span>Loading B</span>
354+
</div>
355+
<div>
356+
<span>Loading C</span>
357+
</div>
358+
</div>,
359+
);
360+
361+
await serverAct(() => C.resolve());
362+
assertLog(['C']);
363+
364+
expect(getVisibleChildren(container)).toEqual(
365+
<div>
366+
<div>
367+
<span>A</span>
368+
<span>B</span>
369+
</div>
370+
<div>
371+
<span>C</span>
372+
</div>
373+
</div>,
374+
);
375+
});
376+
377+
// @gate enableSuspenseList
378+
it('displays all "together" in nested SuspenseLists', async () => {
379+
const A = createAsyncText('A');
380+
const B = createAsyncText('B');
381+
const C = createAsyncText('C');
382+
383+
function Foo() {
384+
return (
385+
<div>
386+
<SuspenseList revealOrder="together">
387+
<Suspense fallback={<Text text="Loading A" />}>
388+
<A />
389+
</Suspense>
390+
<SuspenseList revealOrder="together">
391+
<Suspense fallback={<Text text="Loading B" />}>
392+
<B />
393+
</Suspense>
394+
<Suspense fallback={<Text text="Loading C" />}>
395+
<C />
396+
</Suspense>
397+
</SuspenseList>
398+
</SuspenseList>
399+
</div>
400+
);
401+
}
402+
403+
await A.resolve();
404+
await B.resolve();
405+
406+
await serverAct(async () => {
407+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
408+
pipe(writable);
409+
});
410+
411+
assertLog([
412+
'A',
413+
'B',
414+
'Suspend! [C]',
415+
'Loading A',
416+
'Loading B',
417+
'Loading C',
418+
]);
419+
420+
expect(getVisibleChildren(container)).toEqual(
421+
<div>
422+
<span>Loading A</span>
423+
<span>Loading B</span>
424+
<span>Loading C</span>
425+
</div>,
426+
);
427+
428+
await serverAct(() => C.resolve());
429+
assertLog(['C']);
430+
431+
expect(getVisibleChildren(container)).toEqual(
432+
<div>
433+
<span>A</span>
434+
<span>B</span>
435+
<span>C</span>
436+
</div>,
437+
);
438+
});
439+
440+
// @gate enableSuspenseList
441+
it('displays all "together" in nested SuspenseLists where the inner is default', async () => {
442+
const A = createAsyncText('A');
443+
const B = createAsyncText('B');
444+
const C = createAsyncText('C');
445+
446+
function Foo() {
447+
return (
448+
<div>
449+
<SuspenseList revealOrder="together">
450+
<Suspense fallback={<Text text="Loading A" />}>
451+
<A />
452+
</Suspense>
453+
<SuspenseList>
454+
<Suspense fallback={<Text text="Loading B" />}>
455+
<B />
456+
</Suspense>
457+
<Suspense fallback={<Text text="Loading C" />}>
458+
<C />
459+
</Suspense>
460+
</SuspenseList>
461+
</SuspenseList>
462+
</div>
463+
);
464+
}
465+
466+
await A.resolve();
467+
await B.resolve();
468+
469+
await serverAct(async () => {
470+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
471+
pipe(writable);
472+
});
473+
474+
assertLog([
475+
'A',
476+
'B',
477+
'Suspend! [C]',
478+
'Loading A',
479+
'Loading B',
480+
'Loading C',
481+
]);
482+
483+
expect(getVisibleChildren(container)).toEqual(
484+
<div>
485+
<span>Loading A</span>
486+
<span>Loading B</span>
487+
<span>Loading C</span>
488+
</div>,
489+
);
490+
491+
await serverAct(() => C.resolve());
492+
assertLog(['C']);
493+
494+
expect(getVisibleChildren(container)).toEqual(
495+
<div>
496+
<span>A</span>
497+
<span>B</span>
498+
<span>C</span>
499+
</div>,
500+
);
501+
});
502+
186503
// @gate enableSuspenseList
187504
it('displays each items in "forwards" order', async () => {
188505
const A = createAsyncText('A');

0 commit comments

Comments
 (0)