@samitouri / QOS-React-2 / commits / d742611ce4

Replace Implicit Options on SuspenseList with Explicit Options (#33424)

We want to change the defaults for `revealOrder` and `tail` on SuspenseList. This is an intermediate step to allow experimental users to upgrade. To explicitly specify these options I added `revealOrder="independent"` and `tail="visible"`. I then added warnings if `undefined` or `null` is passed. You must now always explicitly specify them. However, semantics are still preserved for now until the next step. We also want to change the rendering order of the `children` prop for `revealOrder="backwards"`. As an intermediate step I first added `revealOrder="unstable_legacy-backwards"` option. This will only be temporary until all users can switch to the new `"backwards"` semantics once we flip it in the next step. I also clarified the types that the directional props requires iterable children but not iterable inside of those. Rows with multiple items can be modeled as explicit fragments.

Sebastian Markbåge committed Jun 3, 2025 at 17:40 UTC d742611ce40545127032f4e221c78bf9f70eb437
18 files changed +397 -72
.eslintrc.js
+1
@@ -611,6 +611,7 @@ module.exports = {
611 TimeoutID: 'readonly',
612 WheelEventHandler: 'readonly',
613 FinalizationRegistry: 'readonly',
614 + Exclude: 'readonly',
615 Omit: 'readonly',
616 Keyframe: 'readonly',
617 PropertyIndexedKeyframes: 'readonly',
fixtures/ssr/src/components/LargeContent.js
+1 -1
@@ -6,7 +6,7 @@ import React, {
6
7 export default function LargeContent() {
8 return (
9 - <SuspenseList revealOrder="forwards">
9 + <SuspenseList revealOrder="forwards" tail="visible">
10 <Suspense fallback={null}>
11 <p>
12 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+1 -1
@@ -1289,7 +1289,7 @@ describe('ReactDOMFizzServer', () => {
1289 function App({showMore}) {
1290 return (
1291 <div>
1292 - <SuspenseList revealOrder="forwards">
1292 + <SuspenseList revealOrder="forwards" tail="visible">
1293 {a}
1294 {b}
1295 {showMore ? (
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+1 -1
@@ -2254,7 +2254,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
2254 function App() {
2255 return (
2256 <div>
2257 - <SuspenseList revealOrder="forwards">
2257 + <SuspenseList revealOrder="forwards" tail="visible">
2258 <Suspense fallback="Loading A">
2259 <ComponentA />
2260 </Suspense>
packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
+75 -9
@@ -197,6 +197,70 @@ describe('ReactDOMFizzSuspenseList', () => {
197 );
198 });
199
200 + // @gate enableSuspenseList
201 + it('independently with revealOrder="independent"', async () => {
202 + const A = createAsyncText('A');
203 + const B = createAsyncText('B');
204 + const C = createAsyncText('C');
205 +
206 + function Foo() {
207 + return (
208 + <div>
209 + <SuspenseList revealOrder="independent">
210 + <Suspense fallback={<Text text="Loading A" />}>
211 + <A />
212 + </Suspense>
213 + <Suspense fallback={<Text text="Loading B" />}>
214 + <B />
215 + </Suspense>
216 + <Suspense fallback={<Text text="Loading C" />}>
217 + <C />
218 + </Suspense>
219 + </SuspenseList>
220 + </div>
221 + );
222 + }
223 +
224 + await A.resolve();
225 +
226 + await serverAct(async () => {
227 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
228 + pipe(writable);
229 + });
230 +
231 + assertLog(['A', 'Suspend! [B]', 'Suspend! [C]', 'Loading B', 'Loading C']);
232 +
233 + expect(getVisibleChildren(container)).toEqual(
234 + <div>
235 + <span>A</span>
236 + <span>Loading B</span>
237 + <span>Loading C</span>
238 + </div>,
239 + );
240 +
241 + await serverAct(() => C.resolve());
242 + assertLog(['C']);
243 +
244 + expect(getVisibleChildren(container)).toEqual(
245 + <div>
246 + <span>A</span>
247 + <span>Loading B</span>
248 + <span>C</span>
249 + </div>,
250 + );
251 +
252 + await serverAct(() => B.resolve());
253 + assertLog(['B']);
254 +
255 + expect(getVisibleChildren(container)).toEqual(
256 + <div>
257 + <span>A</span>
258 + <span>B</span>
259 + <span>C</span>
260 + </div>,
261 + );
262 + });
263 +
264 // @gate enableSuspenseList
265 it('displays all "together"', async () => {
266 const A = createAsyncText('A');
@@ -452,7 +516,7 @@ describe('ReactDOMFizzSuspenseList', () => {
516 });
517
518 // @gate enableSuspenseList
455 - it('displays all "together" in nested SuspenseLists where the inner is default', async () => {
519 + it('displays all "together" in nested SuspenseLists where the inner is "independent"', async () => {
520 const A = createAsyncText('A');
521 const B = createAsyncText('B');
522 const C = createAsyncText('C');
@@ -464,7 +528,7 @@ describe('ReactDOMFizzSuspenseList', () => {
528 <Suspense fallback={<Text text="Loading A" />}>
529 <A />
530 </Suspense>
467 - <SuspenseList>
531 + <SuspenseList revealOrder="independent">
532 <Suspense fallback={<Text text="Loading B" />}>
533 <B />
534 </Suspense>
@@ -523,7 +587,7 @@ describe('ReactDOMFizzSuspenseList', () => {
587 function Foo() {
588 return (
589 <div>
526 - <SuspenseList revealOrder="forwards">
590 + <SuspenseList revealOrder="forwards" tail="visible">
591 <Suspense fallback={<Text text="Loading A" />}>
592 <A />
593 </Suspense>
@@ -586,7 +650,7 @@ describe('ReactDOMFizzSuspenseList', () => {
650 });
651
652 // @gate enableSuspenseList
589 - it('displays each items in "backwards" order', async () => {
653 + it('displays each items in "backwards" order in legacy mode', async () => {
654 const A = createAsyncText('A');
655 const B = createAsyncText('B');
656 const C = createAsyncText('C');
@@ -594,7 +658,7 @@ describe('ReactDOMFizzSuspenseList', () => {
658 function Foo() {
659 return (
660 <div>
597 - <SuspenseList revealOrder="backwards">
661 + <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
662 <Suspense fallback={<Text text="Loading A" />}>
663 <A />
664 </Suspense>
@@ -665,8 +729,10 @@ describe('ReactDOMFizzSuspenseList', () => {
729 function Foo() {
730 return (
731 <div>
668 - <SuspenseList revealOrder="forwards">
669 - <SuspenseList revealOrder="backwards">
732 + <SuspenseList revealOrder="forwards" tail="visible">
733 + <SuspenseList
734 + revealOrder="unstable_legacy-backwards"
735 + tail="visible">
736 <Suspense fallback={<Text text="Loading A" />}>
737 <A />
738 </Suspense>
@@ -736,7 +802,7 @@ describe('ReactDOMFizzSuspenseList', () => {
802 function Foo() {
803 return (
804 <div>
739 - <SuspenseList revealOrder="forwards">
805 + <SuspenseList revealOrder="forwards" tail="visible">
806 <Suspense fallback={<Text text="Loading A" />}>
807 <A />
808 </Suspense>
@@ -791,7 +857,7 @@ describe('ReactDOMFizzSuspenseList', () => {
857 function Foo() {
858 return (
859 <div>
794 - <SuspenseList revealOrder="forwards">
860 + <SuspenseList revealOrder="forwards" tail="visible">
861 <Suspense fallback={<Text text="Loading A" />}>
862 <A />
863 </Suspense>
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+1 -1
@@ -5755,7 +5755,7 @@ body {
5755 <html>
5756 <body>
5757 <Suspense fallback="loading...">
5758 - <SuspenseList revealOrder="forwards">
5758 + <SuspenseList revealOrder="forwards" tail="visible">
5759 <Suspense fallback="loading foo...">
5760 <BlockedOn value="foo">
5761 <link rel="stylesheet" href="foo" precedence="foo" />
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+1 -1
@@ -2362,7 +2362,7 @@ describe('ReactDOMServerPartialHydration', () => {
2362
2363 function App({showMore}) {
2364 return (
2365 - <SuspenseList revealOrder="forwards">
2365 + <SuspenseList revealOrder="forwards" tail="visible">
2366 {a}
2367 {b}
2368 {showMore ? (
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+1 -1
@@ -123,7 +123,7 @@ describe('ReactDOMServerSuspense', () => {
123 // @gate enableSuspenseList
124 it('server renders a SuspenseList component and its children', async () => {
125 const example = (
126 - <SuspenseList>
126 + <SuspenseList revealOrder="forwards" tail="visible">
127 <React.Suspense fallback="Loading A">
128 <div>A</div>
129 </React.Suspense>
packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js
+1 -1
@@ -172,7 +172,7 @@ test('regression (#20932): return pointer is correct before entering deleted tre
172
173 function App() {
174 return (
175 - <SuspenseList revealOrder="forwards">
175 + <SuspenseList revealOrder="forwards" tail="visible">
176 <Suspense fallback={<Text text="Loading Async..." />}>
177 <Async />
178 </Suspense>
packages/react-reconciler/src/ReactChildFiber.js
+3 -1
@@ -2097,7 +2097,9 @@ export function validateSuspenseListChildren(
2097 ) {
2098 if (__DEV__) {
2099 if (
2100 - (revealOrder === 'forwards' || revealOrder === 'backwards') &&
2100 + (revealOrder === 'forwards' ||
2101 + revealOrder === 'backwards' ||
2102 + revealOrder === 'unstable_legacy-backwards') &&
2103 children !== undefined &&
2104 children !== null &&
2105 children !== false
packages/react-reconciler/src/ReactFiberBeginWork.js
+52 -16
@@ -337,7 +337,7 @@ if (__DEV__) {
337 didWarnAboutContextTypes = ({}: {[string]: boolean});
338 didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean});
339 didWarnAboutReassigningProps = false;
340 - didWarnAboutRevealOrder = ({}: {[empty]: boolean});
340 + didWarnAboutRevealOrder = ({}: {[string]: boolean});
341 didWarnAboutTailOptions = ({}: {[string]: boolean});
342 didWarnAboutDefaultPropsOnFunctionComponent = ({}: {[string]: boolean});
343 didWarnAboutClassNameOnViewTransition = ({}: {[string]: boolean});
@@ -3225,19 +3225,32 @@ function findLastContentRow(firstChild: null | Fiber): null | Fiber {
3225
3226 function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
3227 if (__DEV__) {
3228 + const cacheKey = revealOrder == null ? 'null' : revealOrder;
3229 if (
3229 - revealOrder !== undefined &&
3230 revealOrder !== 'forwards' &&
3231 - revealOrder !== 'backwards' &&
3231 + revealOrder !== 'unstable_legacy-backwards' &&
3232 revealOrder !== 'together' &&
3233 - !didWarnAboutRevealOrder[revealOrder]
3233 + revealOrder !== 'independent' &&
3234 + !didWarnAboutRevealOrder[cacheKey]
3235 ) {
3235 - didWarnAboutRevealOrder[revealOrder] = true;
3236 - if (typeof revealOrder === 'string') {
3236 + didWarnAboutRevealOrder[cacheKey] = true;
3237 + if (revealOrder == null) {
3238 + console.error(
3239 + 'The default for the <SuspenseList revealOrder="..."> prop is changing. ' +
3240 + 'To be future compatible you must explictly specify either ' +
3241 + '"independent" (the current default), "together", "forwards" or "legacy_unstable-backwards".',
3242 + );
3243 + } else if (revealOrder === 'backwards') {
3244 + console.error(
3245 + 'The rendering order of <SuspenseList revealOrder="backwards"> is changing. ' +
3246 + 'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3247 + );
3248 + } else if (typeof revealOrder === 'string') {
3249 switch (revealOrder.toLowerCase()) {
3250 case 'together':
3251 case 'forwards':
3240 - case 'backwards': {
3252 + case 'backwards':
3253 + case 'independent': {
3254 console.error(
3255 '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
3256 'Use lowercase "%s" instead.',
@@ -3259,7 +3272,7 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
3272 default:
3273 console.error(
3274 '"%s" is not a supported revealOrder on <SuspenseList />. ' +
3262 - 'Did you mean "together", "forwards" or "backwards"?',
3275 + 'Did you mean "independent", "together", "forwards" or "backwards"?',
3276 revealOrder,
3277 );
3278 break;
@@ -3267,7 +3280,7 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
3280 } else {
3281 console.error(
3282 '%s is not a supported value for revealOrder on <SuspenseList />. ' +
3270 - 'Did you mean "together", "forwards" or "backwards"?',
3283 + 'Did you mean "independent", "together", "forwards" or "backwards"?',
3284 revealOrder,
3285 );
3286 }
@@ -3280,16 +3293,38 @@ function validateTailOptions(
3293 revealOrder: SuspenseListRevealOrder,
3294 ) {
3295 if (__DEV__) {
3283 - if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) {
3284 - if (tailMode !== 'collapsed' && tailMode !== 'hidden') {
3285 - didWarnAboutTailOptions[tailMode] = true;
3296 + const cacheKey = tailMode == null ? 'null' : tailMode;
3297 + if (!didWarnAboutTailOptions[cacheKey]) {
3298 + if (tailMode == null) {
3299 + if (
3300 + revealOrder === 'forwards' ||
3301 + revealOrder === 'backwards' ||
3302 + revealOrder === 'unstable_legacy-backwards'
3303 + ) {
3304 + didWarnAboutTailOptions[cacheKey] = true;
3305 + console.error(
3306 + 'The default for the <SuspenseList tail="..."> prop is changing. ' +
3307 + 'To be future compatible you must explictly specify either ' +
3308 + '"visible" (the current default), "collapsed" or "hidden".',
3309 + );
3310 + }
3311 + } else if (
3312 + tailMode !== 'visible' &&
3313 + tailMode !== 'collapsed' &&
3314 + tailMode !== 'hidden'
3315 + ) {
3316 + didWarnAboutTailOptions[cacheKey] = true;
3317 console.error(
3318 '"%s" is not a supported value for tail on <SuspenseList />. ' +
3288 - 'Did you mean "collapsed" or "hidden"?',
3319 + 'Did you mean "visible", "collapsed" or "hidden"?',
3320 tailMode,
3321 );
3291 - } else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') {
3292 - didWarnAboutTailOptions[tailMode] = true;
3322 + } else if (
3323 + revealOrder !== 'forwards' &&
3324 + revealOrder !== 'backwards' &&
3325 + revealOrder !== 'unstable_legacy-backwards'
3326 + ) {
3327 + didWarnAboutTailOptions[cacheKey] = true;
3328 console.error(
3329 '<SuspenseList tail="%s" /> is only valid if revealOrder is ' +
3330 '"forwards" or "backwards". ' +
@@ -3414,7 +3449,8 @@ function updateSuspenseListComponent(
3449 );
3450 break;
3451 }
3417 - case 'backwards': {
3452 + case 'backwards':
3453 + case 'unstable_legacy-backwards': {
3454 // We're going to find the first row that has existing content.
3455 // At the same time we're going to reverse the list of everything
3456 // we pass in the meantime. That's going to be our tail in reverse
packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+5 -2
@@ -75,9 +75,12 @@ export function findFirstSuspended(row: Fiber): null | Fiber {
75 }
76 } else if (
77 node.tag === SuspenseListComponent &&
78 - // revealOrder undefined can't be trusted because it don't
78 + // Independent revealOrder can't be trusted because it doesn't
79 // keep track of whether it suspended or not.
80 - node.memoizedProps.revealOrder !== undefined
80 + (node.memoizedProps.revealOrder === 'forwards' ||
81 + node.memoizedProps.revealOrder === 'backwards' ||
82 + node.memoizedProps.revealOrder === 'unstable_legacy-backwards' ||
83 + node.memoizedProps.revealOrder === 'together')
84 ) {
85 const didSuspend = (node.flags & DidCapture) !== NoFlags;
86 if (didSuspend) {
packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js
+1 -1
@@ -677,7 +677,7 @@ describe('ReactLazyContextPropagation', () => {
677 setContext = setValue;
678 const children = React.useMemo(
679 () => (
680 - <SuspenseList revealOrder="forwards">
680 + <SuspenseList revealOrder="forwards" tail="visible">
681 <Child />
682 <Child />
683 </SuspenseList>
packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
+1 -1
@@ -255,7 +255,7 @@ describe('ReactFragment', () => {
255 onCaughtError,
256 }).render(
257 <CatchingBoundary>
258 - <SuspenseList>
258 + <SuspenseList revealOrder="independent">
259 <SomethingThatErrors />
260 </SuspenseList>
261 </CatchingBoundary>,
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+215 -25
@@ -79,7 +79,7 @@ describe('ReactSuspenseList', () => {
79 });
80 assertConsoleErrorDev([
81 '"something" is not a supported revealOrder on ' +
82 - '<SuspenseList />. Did you mean "together", "forwards" or "backwards"?' +
82 + '<SuspenseList />. Did you mean "independent", "together", "forwards" or "backwards"?' +
83 '\n in SuspenseList (at **)' +
84 '\n in Foo (at **)',
85 ]);
@@ -131,7 +131,11 @@ describe('ReactSuspenseList', () => {
131 // @gate enableSuspenseList
132 it('warns if a single element is passed to a "forwards" list', async () => {
133 function Foo({children}) {
134 - return <SuspenseList revealOrder="forwards">{children}</SuspenseList>;
134 + return (
135 + <SuspenseList revealOrder="forwards" tail="visible">
136 + {children}
137 + </SuspenseList>
138 + );
139 }
140
141 ReactNoop.render(<Foo />);
@@ -166,7 +170,7 @@ describe('ReactSuspenseList', () => {
170 it('warns if a single fragment is passed to a "backwards" list', async () => {
171 function Foo() {
172 return (
169 - <SuspenseList revealOrder="backwards">
173 + <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
174 <>{[]}</>
175 </SuspenseList>
176 );
@@ -176,7 +180,7 @@ describe('ReactSuspenseList', () => {
180 ReactNoop.render(<Foo />);
181 });
182 assertConsoleErrorDev([
179 - 'A single row was passed to a <SuspenseList revealOrder="backwards" />. ' +
183 + 'A single row was passed to a <SuspenseList revealOrder="unstable_legacy-backwards" />. ' +
184 'This is not useful since it needs multiple rows. ' +
185 'Did you mean to pass multiple children or an array?' +
186 '\n in SuspenseList (at **)' +
@@ -188,7 +192,7 @@ describe('ReactSuspenseList', () => {
192 it('warns if a nested array is passed to a "forwards" list', async () => {
193 function Foo({items}) {
194 return (
191 - <SuspenseList revealOrder="forwards">
195 + <SuspenseList revealOrder="forwards" tail="visible">
196 {items.map(name => (
197 <Suspense key={name} fallback="Loading">
198 {name}
@@ -214,7 +218,7 @@ describe('ReactSuspenseList', () => {
218 });
219
220 // @gate enableSuspenseList
217 - it('shows content independently by default', async () => {
221 + it('warns if no revealOrder is specified', async () => {
222 const A = createAsyncText('A');
223 const B = createAsyncText('B');
224 const C = createAsyncText('C');
@@ -250,6 +254,86 @@ describe('ReactSuspenseList', () => {
254 'Suspend! [C]',
255 ]);
256
257 + assertConsoleErrorDev([
258 + 'The default for the <SuspenseList revealOrder="..."> prop is changing. ' +
259 + 'To be future compatible you must explictly specify either ' +
260 + '"independent" (the current default), "together", "forwards" or "legacy_unstable-backwards".' +
261 + '\n in SuspenseList (at **)' +
262 + '\n in Foo (at **)',
263 + ]);
264 +
265 + expect(ReactNoop).toMatchRenderedOutput(
266 + <>
267 + <span>A</span>
268 + <span>Loading B</span>
269 + <span>Loading C</span>
270 + </>,
271 + );
272 +
273 + await act(() => C.resolve());
274 + assertLog(
275 + gate('alwaysThrottleRetries')
276 + ? ['Suspend! [B]', 'C', 'Suspend! [B]']
277 + : ['C'],
278 + );
279 +
280 + expect(ReactNoop).toMatchRenderedOutput(
281 + <>
282 + <span>A</span>
283 + <span>Loading B</span>
284 + <span>C</span>
285 + </>,
286 + );
287 +
288 + await act(() => B.resolve());
289 + assertLog(['B']);
290 +
291 + expect(ReactNoop).toMatchRenderedOutput(
292 + <>
293 + <span>A</span>
294 + <span>B</span>
295 + <span>C</span>
296 + </>,
297 + );
298 + });
299 +
300 + // @gate enableSuspenseList
301 + it('shows content independently with revealOrder="independent"', async () => {
302 + const A = createAsyncText('A');
303 + const B = createAsyncText('B');
304 + const C = createAsyncText('C');
305 +
306 + function Foo() {
307 + return (
308 + <SuspenseList revealOrder="independent">
309 + <Suspense fallback={<Text text="Loading A" />}>
310 + <A />
311 + </Suspense>
312 + <Suspense fallback={<Text text="Loading B" />}>
313 + <B />
314 + </Suspense>
315 + <Suspense fallback={<Text text="Loading C" />}>
316 + <C />
317 + </Suspense>
318 + </SuspenseList>
319 + );
320 + }
321 +
322 + await A.resolve();
323 +
324 + ReactNoop.render(<Foo />);
325 +
326 + await waitForAll([
327 + 'A',
328 + 'Suspend! [B]',
329 + 'Loading B',
330 + 'Suspend! [C]',
331 + 'Loading C',
332 + // pre-warming
333 + 'Suspend! [B]',
334 + 'Suspend! [C]',
335 + ]);
336 +
337 expect(ReactNoop).toMatchRenderedOutput(
338 <>
339 <span>A</span>
@@ -564,7 +648,7 @@ describe('ReactSuspenseList', () => {
648 });
649
650 // @gate enableSuspenseList
567 - it('displays all "together" in nested SuspenseLists where the inner is default', async () => {
651 + it('displays all "together" in nested SuspenseLists where the inner is "independent"', async () => {
652 const A = createAsyncText('A');
653 const B = createAsyncText('B');
654 const C = createAsyncText('C');
@@ -575,7 +659,7 @@ describe('ReactSuspenseList', () => {
659 <Suspense fallback={<Text text="Loading A" />}>
660 <A />
661 </Suspense>
578 - <SuspenseList>
662 + <SuspenseList revealOrder="independent">
663 <Suspense fallback={<Text text="Loading B" />}>
664 <B />
665 </Suspense>
@@ -897,7 +981,7 @@ describe('ReactSuspenseList', () => {
981
982 function Foo() {
983 return (
900 - <SuspenseList revealOrder="forwards">
984 + <SuspenseList revealOrder="forwards" tail="visible">
985 <Suspense fallback={<Text text="Loading A" />}>
986 <A />
987 </Suspense>
@@ -955,6 +1039,85 @@ describe('ReactSuspenseList', () => {
1039 );
1040 });
1041
1042 + // @gate enableSuspenseList
1043 + it('warns if revealOrder="backwards" is specified', async () => {
1044 + const A = createAsyncText('A');
1045 + const B = createAsyncText('B');
1046 + const C = createAsyncText('C');
1047 +
1048 + function Foo() {
1049 + return (
1050 + <SuspenseList revealOrder="backwards" tail="visible">
1051 + <Suspense fallback={<Text text="Loading A" />}>
1052 + <A />
1053 + </Suspense>
1054 + <Suspense fallback={<Text text="Loading B" />}>
1055 + <B />
1056 + </Suspense>
1057 + <Suspense fallback={<Text text="Loading C" />}>
1058 + <C />
1059 + </Suspense>
1060 + </SuspenseList>
1061 + );
1062 + }
1063 +
1064 + await A.resolve();
1065 +
1066 + ReactNoop.render(<Foo />);
1067 +
1068 + await waitForAll([
1069 + 'Suspend! [C]',
1070 + 'Loading C',
1071 + 'Loading B',
1072 + 'Loading A',
1073 + // pre-warming
1074 + 'Suspend! [C]',
1075 + ]);
1076 +
1077 + assertConsoleErrorDev([
1078 + 'The rendering order of <SuspenseList revealOrder="backwards"> is changing. ' +
1079 + 'To be future compatible you must specify ' +
1080 + 'revealOrder="legacy_unstable-backwards" instead.' +
1081 + '\n in SuspenseList (at **)' +
1082 + '\n in Foo (at **)',
1083 + ]);
1084 +
1085 + expect(ReactNoop).toMatchRenderedOutput(
1086 + <>
1087 + <span>Loading A</span>
1088 + <span>Loading B</span>
1089 + <span>Loading C</span>
1090 + </>,
1091 + );
1092 +
1093 + await act(() => C.resolve());
1094 + assertLog([
1095 + 'C',
1096 + 'Suspend! [B]',
1097 + // pre-warming
1098 + 'Suspend! [B]',
1099 + ]);
1100 +
1101 + expect(ReactNoop).toMatchRenderedOutput(
1102 + <>
1103 + <span>Loading A</span>
1104 + <span>Loading B</span>
1105 + <span>C</span>
1106 + </>,
1107 + );
1108 +
1109 + await act(() => B.resolve());
1110 + assertLog(['B', 'A']);
1111 +
1112 + expect(ReactNoop).toMatchRenderedOutput(
1113 + <>
1114 + <span>A</span>
1115 + <span>B</span>
1116 + <span>C</span>
1117 + </>,
1118 + );
1119 + });
1120 +
1121 // @gate enableSuspenseList
1122 it('displays each items in "backwards" order', async () => {
1123 const A = createAsyncText('A');
@@ -963,7 +1126,7 @@ describe('ReactSuspenseList', () => {
1126
1127 function Foo() {
1128 return (
966 - <SuspenseList revealOrder="backwards">
1129 + <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
1130 <Suspense fallback={<Text text="Loading A" />}>
1131 <A />
1132 </Suspense>
@@ -1037,7 +1200,7 @@ describe('ReactSuspenseList', () => {
1200
1201 function Foo({items}) {
1202 return (
1040 - <SuspenseList revealOrder="forwards">
1203 + <SuspenseList revealOrder="forwards" tail="visible">
1204 {items.map(([key, Component]) => (
1205 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1206 <Component />
@@ -1222,7 +1385,7 @@ describe('ReactSuspenseList', () => {
1385
1386 function Foo({items}) {
1387 return (
1225 - <SuspenseList revealOrder="backwards">
1388 + <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
1389 {items.map(([key, Component]) => (
1390 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1391 <Component />
@@ -1400,7 +1563,7 @@ describe('ReactSuspenseList', () => {
1563 it('switches to rendering fallbacks if the tail takes long CPU time', async () => {
1564 function Foo() {
1565 return (
1403 - <SuspenseList revealOrder="forwards">
1566 + <SuspenseList revealOrder="forwards" tail="visible">
1567 <Suspense fallback={<Text text="Loading A" />}>
1568 <Text text="A" />
1569 </Suspense>
@@ -1535,6 +1698,29 @@ describe('ReactSuspenseList', () => {
1698 );
1699 });
1700
1701 + // @gate enableSuspenseList
1702 + it('warns if no tail option is specified', async () => {
1703 + function Foo() {
1704 + return (
1705 + <SuspenseList revealOrder="forwards">
1706 + <Suspense fallback="Loading">A</Suspense>
1707 + <Suspense fallback="Loading">B</Suspense>
1708 + </SuspenseList>
1709 + );
1710 + }
1711 +
1712 + await act(() => {
1713 + ReactNoop.render(<Foo />);
1714 + });
1715 + assertConsoleErrorDev([
1716 + 'The default for the <SuspenseList tail="..."> prop is changing. ' +
1717 + 'To be future compatible you must explictly specify either ' +
1718 + '"visible" (the current default), "collapsed" or "hidden".' +
1719 + '\n in SuspenseList (at **)' +
1720 + '\n in Foo (at **)',
1721 + ]);
1722 + });
1723 +
1724 // @gate enableSuspenseList
1725 it('warns if an unsupported tail option is used', async () => {
1726 function Foo() {
@@ -1551,7 +1737,7 @@ describe('ReactSuspenseList', () => {
1737 });
1738 assertConsoleErrorDev([
1739 '"collapse" is not a supported value for tail on ' +
1554 - '<SuspenseList />. Did you mean "collapsed" or "hidden"?' +
1740 + '<SuspenseList />. Did you mean "visible", "collapsed" or "hidden"?' +
1741 '\n in SuspenseList (at **)' +
1742 '\n in Foo (at **)',
1743 ]);
@@ -1796,7 +1982,7 @@ describe('ReactSuspenseList', () => {
1982
1983 function Foo({items}) {
1984 return (
1799 - <SuspenseList revealOrder="backwards" tail="collapsed">
1985 + <SuspenseList revealOrder="unstable_legacy-backwards" tail="collapsed">
1986 {items.map(([key, Component]) => (
1987 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1988 <Component />
@@ -2154,7 +2340,7 @@ describe('ReactSuspenseList', () => {
2340 function Foo() {
2341 return (
2342 <SuspenseList revealOrder="together">
2157 - <SuspenseList revealOrder="forwards">
2343 + <SuspenseList revealOrder="forwards" tail="visible">
2344 <Suspense fallback={<Text text="Loading A" />}>
2345 <Text text="A" />
2346 </Suspense>
@@ -2255,7 +2441,7 @@ describe('ReactSuspenseList', () => {
2441
2442 function Foo({showB}) {
2443 return (
2258 - <SuspenseList revealOrder="forwards">
2444 + <SuspenseList revealOrder="forwards" tail="visible">
2445 <SuspenseList revealOrder="forwards" tail="hidden">
2446 <Suspense fallback={<Text text="Loading A" />}>
2447 <Text text="A" />
@@ -2321,7 +2507,7 @@ describe('ReactSuspenseList', () => {
2507 function Foo() {
2508 return (
2509 <div>
2324 - <SuspenseList revealOrder="forwards">
2510 + <SuspenseList revealOrder="forwards" tail="visible">
2511 <Text text="A" />
2512 <Text text="B" />
2513 </SuspenseList>
@@ -2673,7 +2859,7 @@ describe('ReactSuspenseList', () => {
2859 function App() {
2860 Scheduler.log('App');
2861 return (
2676 - <SuspenseList revealOrder="forwards">
2862 + <SuspenseList revealOrder="forwards" tail="visible">
2863 <Suspense fallback={<Text text="Loading A" />}>
2864 <Sleep time={600}>
2865 <TwoPass text="A" />
@@ -2760,7 +2946,7 @@ describe('ReactSuspenseList', () => {
2946 Scheduler.log('App');
2947 return (
2948 <Profiler id="root" onRender={onRender}>
2763 - <SuspenseList revealOrder="forwards">
2949 + <SuspenseList revealOrder="forwards" tail="visible">
2950 <Suspense fallback={<Fallback />}>
2951 <Sleep time={1}>
2952 <A />
@@ -2936,7 +3122,7 @@ describe('ReactSuspenseList', () => {
3122 // Several layers of Bailout wrappers help verify we're
3123 // marking updates all the way to the propagation root.
3124 return (
2939 - <SuspenseList revealOrder="forwards">
3125 + <SuspenseList revealOrder="forwards" tail="visible">
3126 <Bailout>
3127 <Bailout>
3128 <Bailout>
@@ -3029,7 +3215,7 @@ describe('ReactSuspenseList', () => {
3215
3216 function Repro({update}) {
3217 return (
3032 - <SuspenseList revealOrder="forwards">
3218 + <SuspenseList revealOrder="forwards" tail="visible">
3219 {update && (
3220 <Suspense fallback={<Text text="Loading A..." />}>
3221 <A />
@@ -3128,7 +3314,7 @@ describe('ReactSuspenseList', () => {
3314 }
3315 function Foo() {
3316 return (
3131 - <SuspenseList revealOrder="forwards">
3317 + <SuspenseList revealOrder="forwards" tail="visible">
3318 <Generator />
3319 </SuspenseList>
3320 );
@@ -3184,7 +3370,11 @@ describe('ReactSuspenseList', () => {
3370 };
3371
3372 function Foo() {
3187 - return <SuspenseList revealOrder="forwards">{iterable}</SuspenseList>;
3373 + return (
3374 + <SuspenseList revealOrder="forwards" tail="visible">
3375 + {iterable}
3376 + </SuspenseList>
3377 + );
3378 }
3379
3380 await act(() => {
@@ -3270,7 +3460,7 @@ describe('ReactSuspenseList', () => {
3460 it('warns if a nested async iterable is passed to a "forwards" list', async () => {
3461 function Foo({items}) {
3462 return (
3273 - <SuspenseList revealOrder="forwards">
3463 + <SuspenseList revealOrder="forwards" tail="visible">
3464 {items}
3465 <div>Tail</div>
3466 </SuspenseList>
packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js
+1 -1
@@ -345,7 +345,7 @@ describe('ReactSuspenseyCommitPhase', () => {
345 it('demonstrate current behavior when used with SuspenseList (not ideal)', async () => {
346 function App() {
347 return (
348 - <SuspenseList revealOrder="forwards">
348 + <SuspenseList revealOrder="forwards" tail="visible">
349 <Suspense fallback={<Text text="Loading A" />}>
350 <SuspenseyImage src="A" />
351 </Suspense>
packages/react-server/src/ReactFizzServer.js
+20 -5
@@ -1799,7 +1799,7 @@ function renderSuspenseListRows(
1799 task: Task,
1800 keyPath: KeyNode,
1801 rows: Array<ReactNodeList>,
1802 - revealOrder: 'forwards' | 'backwards',
1802 + revealOrder: 'forwards' | 'backwards' | 'unstable_legacy-backwards',
1803 ): void {
1804 // This is a fork of renderChildrenArray that's aware of tracking rows.
1805 const prevKeyPath = task.keyPath;
@@ -1827,7 +1827,11 @@ function renderSuspenseListRows(
1827 // Since we are going to resume into a slot whose order was already
1828 // determined by the prerender, we can safely resume it even in reverse
1829 // render order.
1830 - const i = revealOrder !== 'backwards' ? n : totalChildren - 1 - n;
1830 + const i =
1831 + revealOrder !== 'backwards' &&
1832 + revealOrder !== 'unstable_legacy-backwards'
1833 + ? n
1834 + : totalChildren - 1 - n;
1835 const node = rows[i];
1836 task.row = previousSuspenseListRow = createSuspenseListRow(
1837 previousSuspenseListRow,
@@ -1852,7 +1856,11 @@ function renderSuspenseListRows(
1856 // Since we are going to resume into a slot whose order was already
1857 // determined by the prerender, we can safely resume it even in reverse
1858 // render order.
1855 - const i = revealOrder !== 'backwards' ? n : totalChildren - 1 - n;
1859 + const i =
1860 + revealOrder !== 'backwards' &&
1861 + revealOrder !== 'unstable_legacy-backwards'
1862 + ? n
1863 + : totalChildren - 1 - n;
1864 const node = rows[i];
1865 if (__DEV__) {
1866 warnForMissingKey(request, task, node);
@@ -1869,7 +1877,10 @@ function renderSuspenseListRows(
1877 }
1878 } else {
1879 task = ((task: any): RenderTask); // Refined
1872 - if (revealOrder !== 'backwards') {
1880 + if (
1881 + revealOrder !== 'backwards' &&
1882 + revealOrder !== 'unstable_legacy-backwards'
1883 + ) {
1884 // Forwards direction
1885 for (let i = 0; i < totalChildren; i++) {
1886 const node = rows[i];
@@ -1973,7 +1984,11 @@ function renderSuspenseList(
1984 const revealOrder: SuspenseListRevealOrder = props.revealOrder;
1985 // TODO: Support tail hidden/collapsed modes.
1986 // const tailMode: SuspenseListTailMode = props.tail;
1976 - if (revealOrder === 'forwards' || revealOrder === 'backwards') {
1987 + if (
1988 + revealOrder === 'forwards' ||
1989 + revealOrder === 'backwards' ||
1990 + revealOrder === 'unstable_legacy-backwards'
1991 + ) {
1992 // For ordered reveal, we need to produce rows from the children.
1993 if (isArray(children)) {
1994 renderSuspenseListRows(request, task, keyPath, children, revealOrder);
packages/shared/ReactTypes.js
+16 -4
@@ -308,20 +308,32 @@ export type SuspenseProps = {
308 export type SuspenseListRevealOrder =
309 | 'forwards'
310 | 'backwards'
311 + | 'unstable_legacy-backwards'
312 | 'together'
313 + | 'independent'
314 | void;
315
314 -export type SuspenseListTailMode = 'collapsed' | 'hidden' | void;
316 +export type SuspenseListTailMode = 'visible' | 'collapsed' | 'hidden' | void;
317 +
318 +// A SuspenseList row cannot include a nested Array since it's an easy mistake to not realize it
319 +// is treated as a single row. A Fragment can be used to intentionally have multiple children as
320 +// a single row.
321 +type SuspenseListRow = Exclude<
322 + ReactNodeList,
323 + Iterable<React$Node> | AsyncIterable<React$Node>,
324 +>;
325
326 type DirectionalSuspenseListProps = {
317 - children?: ReactNodeList,
318 - revealOrder: 'forwards' | 'backwards',
327 + // Directional SuspenseList are defined by an array of children or multiple slots to JSX
328 + // It does not allow a single element child.
329 + children?: Iterable<SuspenseListRow> | AsyncIterable<SuspenseListRow>, // Note: AsyncIterable is experimental.
330 + revealOrder: 'forwards' | 'backwards' | 'unstable_legacy-backwards',
331 tail?: SuspenseListTailMode,
332 };
333
334 type NonDirectionalSuspenseListProps = {
335 children?: ReactNodeList,
324 - revealOrder?: 'together' | void,
336 + revealOrder?: 'independent' | 'together' | void,
337 tail?: void,
338 };
339