diff --git a/.eslintrc.js b/.eslintrc.js index 1bb4e868d0..8ffc5a732c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -611,6 +611,7 @@ module.exports = { TimeoutID: 'readonly', WheelEventHandler: 'readonly', FinalizationRegistry: 'readonly', + Exclude: 'readonly', Omit: 'readonly', Keyframe: 'readonly', PropertyIndexedKeyframes: 'readonly', diff --git a/fixtures/ssr/src/components/LargeContent.js b/fixtures/ssr/src/components/LargeContent.js index 7c4a6cf225..f5c8adb03e 100644 --- a/fixtures/ssr/src/components/LargeContent.js +++ b/fixtures/ssr/src/components/LargeContent.js @@ -6,7 +6,7 @@ import React, { export default function LargeContent() { return ( - +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index d5f635f964..57124ec6e0 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1289,7 +1289,7 @@ describe('ReactDOMFizzServer', () => { function App({showMore}) { return (

- + {a} {b} {showMore ? ( diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 478bed90a3..c306b1369b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -2254,7 +2254,7 @@ describe('ReactDOMFizzStaticBrowser', () => { function App() { return (
- + diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js index 977d2dbf15..74db51d242 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js @@ -197,6 +197,70 @@ describe('ReactDOMFizzSuspenseList', () => { ); }); + // @gate enableSuspenseList + it('independently with revealOrder="independent"', async () => { + const A = createAsyncText('A'); + const B = createAsyncText('B'); + const C = createAsyncText('C'); + + function Foo() { + return ( + + ); + } + + await A.resolve(); + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + + assertLog(['A', 'Suspend! [B]', 'Suspend! [C]', 'Loading B', 'Loading C']); + + expect(getVisibleChildren(container)).toEqual( +
+ A + Loading B + Loading C +
, + ); + + await serverAct(() => C.resolve()); + assertLog(['C']); + + expect(getVisibleChildren(container)).toEqual( +
+ A + Loading B + C +
, + ); + + await serverAct(() => B.resolve()); + assertLog(['B']); + + expect(getVisibleChildren(container)).toEqual( +
+ A + B + C +
, + ); + }); + // @gate enableSuspenseList it('displays all "together"', async () => { const A = createAsyncText('A'); @@ -452,7 +516,7 @@ describe('ReactDOMFizzSuspenseList', () => { }); // @gate enableSuspenseList - it('displays all "together" in nested SuspenseLists where the inner is default', async () => { + it('displays all "together" in nested SuspenseLists where the inner is "independent"', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); @@ -464,7 +528,7 @@ describe('ReactDOMFizzSuspenseList', () => { }>
- + }> @@ -523,7 +587,7 @@ describe('ReactDOMFizzSuspenseList', () => { function Foo() { return (
- + }> @@ -586,7 +650,7 @@ describe('ReactDOMFizzSuspenseList', () => { }); // @gate enableSuspenseList - it('displays each items in "backwards" order', async () => { + it('displays each items in "backwards" order in legacy mode', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); @@ -594,7 +658,7 @@ describe('ReactDOMFizzSuspenseList', () => { function Foo() { return (
- + }> @@ -665,8 +729,10 @@ describe('ReactDOMFizzSuspenseList', () => { function Foo() { return (
- - + + }> @@ -736,7 +802,7 @@ describe('ReactDOMFizzSuspenseList', () => { function Foo() { return (
- + }> @@ -791,7 +857,7 @@ describe('ReactDOMFizzSuspenseList', () => { function Foo() { return (
- + }> diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 6436f1898b..b8a4e5b86a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -5755,7 +5755,7 @@ body { - + diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 8cace332cd..5b719ddf4a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2362,7 +2362,7 @@ describe('ReactDOMServerPartialHydration', () => { function App({showMore}) { return ( - + {a} {b} {showMore ? ( diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js index 8a959a2949..6e6f9bb092 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js @@ -123,7 +123,7 @@ describe('ReactDOMServerSuspense', () => { // @gate enableSuspenseList it('server renders a SuspenseList component and its children', async () => { const example = ( - +
A
diff --git a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js index e221f01b92..0c73410f2e 100644 --- a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js +++ b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js @@ -172,7 +172,7 @@ test('regression (#20932): return pointer is correct before entering deleted tre function App() { return ( - + }> diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index fcb2406552..f574162b41 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -2097,7 +2097,9 @@ export function validateSuspenseListChildren( ) { if (__DEV__) { if ( - (revealOrder === 'forwards' || revealOrder === 'backwards') && + (revealOrder === 'forwards' || + revealOrder === 'backwards' || + revealOrder === 'unstable_legacy-backwards') && children !== undefined && children !== null && children !== false diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 7b86962f77..a931616315 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -337,7 +337,7 @@ if (__DEV__) { didWarnAboutContextTypes = ({}: {[string]: boolean}); didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean}); didWarnAboutReassigningProps = false; - didWarnAboutRevealOrder = ({}: {[empty]: boolean}); + didWarnAboutRevealOrder = ({}: {[string]: boolean}); didWarnAboutTailOptions = ({}: {[string]: boolean}); didWarnAboutDefaultPropsOnFunctionComponent = ({}: {[string]: boolean}); didWarnAboutClassNameOnViewTransition = ({}: {[string]: boolean}); @@ -3225,19 +3225,32 @@ function findLastContentRow(firstChild: null | Fiber): null | Fiber { function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { if (__DEV__) { + const cacheKey = revealOrder == null ? 'null' : revealOrder; if ( - revealOrder !== undefined && revealOrder !== 'forwards' && - revealOrder !== 'backwards' && + revealOrder !== 'unstable_legacy-backwards' && revealOrder !== 'together' && - !didWarnAboutRevealOrder[revealOrder] + revealOrder !== 'independent' && + !didWarnAboutRevealOrder[cacheKey] ) { - didWarnAboutRevealOrder[revealOrder] = true; - if (typeof revealOrder === 'string') { + didWarnAboutRevealOrder[cacheKey] = true; + if (revealOrder == null) { + console.error( + 'The default for the prop is changing. ' + + 'To be future compatible you must explictly specify either ' + + '"independent" (the current default), "together", "forwards" or "legacy_unstable-backwards".', + ); + } else if (revealOrder === 'backwards') { + console.error( + 'The rendering order of is changing. ' + + 'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.', + ); + } else if (typeof revealOrder === 'string') { switch (revealOrder.toLowerCase()) { case 'together': case 'forwards': - case 'backwards': { + case 'backwards': + case 'independent': { console.error( '"%s" is not a valid value for revealOrder on . ' + 'Use lowercase "%s" instead.', @@ -3259,7 +3272,7 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { default: console.error( '"%s" is not a supported revealOrder on . ' + - 'Did you mean "together", "forwards" or "backwards"?', + 'Did you mean "independent", "together", "forwards" or "backwards"?', revealOrder, ); break; @@ -3267,7 +3280,7 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { } else { console.error( '%s is not a supported value for revealOrder on . ' + - 'Did you mean "together", "forwards" or "backwards"?', + 'Did you mean "independent", "together", "forwards" or "backwards"?', revealOrder, ); } @@ -3280,16 +3293,38 @@ function validateTailOptions( revealOrder: SuspenseListRevealOrder, ) { if (__DEV__) { - if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) { - if (tailMode !== 'collapsed' && tailMode !== 'hidden') { - didWarnAboutTailOptions[tailMode] = true; + const cacheKey = tailMode == null ? 'null' : tailMode; + if (!didWarnAboutTailOptions[cacheKey]) { + if (tailMode == null) { + if ( + revealOrder === 'forwards' || + revealOrder === 'backwards' || + revealOrder === 'unstable_legacy-backwards' + ) { + didWarnAboutTailOptions[cacheKey] = true; + console.error( + 'The default for the prop is changing. ' + + 'To be future compatible you must explictly specify either ' + + '"visible" (the current default), "collapsed" or "hidden".', + ); + } + } else if ( + tailMode !== 'visible' && + tailMode !== 'collapsed' && + tailMode !== 'hidden' + ) { + didWarnAboutTailOptions[cacheKey] = true; console.error( '"%s" is not a supported value for tail on . ' + - 'Did you mean "collapsed" or "hidden"?', + 'Did you mean "visible", "collapsed" or "hidden"?', tailMode, ); - } else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') { - didWarnAboutTailOptions[tailMode] = true; + } else if ( + revealOrder !== 'forwards' && + revealOrder !== 'backwards' && + revealOrder !== 'unstable_legacy-backwards' + ) { + didWarnAboutTailOptions[cacheKey] = true; console.error( ' is only valid if revealOrder is ' + '"forwards" or "backwards". ' + @@ -3414,7 +3449,8 @@ function updateSuspenseListComponent( ); break; } - case 'backwards': { + case 'backwards': + case 'unstable_legacy-backwards': { // We're going to find the first row that has existing content. // At the same time we're going to reverse the list of everything // we pass in the meantime. That's going to be our tail in reverse diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index f7ae78ed6d..2542d660c4 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -75,9 +75,12 @@ export function findFirstSuspended(row: Fiber): null | Fiber { } } else if ( node.tag === SuspenseListComponent && - // revealOrder undefined can't be trusted because it don't + // Independent revealOrder can't be trusted because it doesn't // keep track of whether it suspended or not. - node.memoizedProps.revealOrder !== undefined + (node.memoizedProps.revealOrder === 'forwards' || + node.memoizedProps.revealOrder === 'backwards' || + node.memoizedProps.revealOrder === 'unstable_legacy-backwards' || + node.memoizedProps.revealOrder === 'together') ) { const didSuspend = (node.flags & DidCapture) !== NoFlags; if (didSuspend) { diff --git a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js index 6ed19ba6d5..0a338c8aa8 100644 --- a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js +++ b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js @@ -677,7 +677,7 @@ describe('ReactLazyContextPropagation', () => { setContext = setValue; const children = React.useMemo( () => ( - + diff --git a/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js b/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js index 1b8beba58b..9e4e9694a6 100644 --- a/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js +++ b/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js @@ -255,7 +255,7 @@ describe('ReactFragment', () => { onCaughtError, }).render( - + , diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index f9efb330cf..bfbf165dba 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -79,7 +79,7 @@ describe('ReactSuspenseList', () => { }); assertConsoleErrorDev([ '"something" is not a supported revealOrder on ' + - '. Did you mean "together", "forwards" or "backwards"?' + + '. Did you mean "independent", "together", "forwards" or "backwards"?' + '\n in SuspenseList (at **)' + '\n in Foo (at **)', ]); @@ -131,7 +131,11 @@ describe('ReactSuspenseList', () => { // @gate enableSuspenseList it('warns if a single element is passed to a "forwards" list', async () => { function Foo({children}) { - return {children}; + return ( + + {children} + + ); } ReactNoop.render(); @@ -166,7 +170,7 @@ describe('ReactSuspenseList', () => { it('warns if a single fragment is passed to a "backwards" list', async () => { function Foo() { return ( - + <>{[]} ); @@ -176,7 +180,7 @@ describe('ReactSuspenseList', () => { ReactNoop.render(); }); assertConsoleErrorDev([ - 'A single row was passed to a . ' + + 'A single row was passed to a . ' + 'This is not useful since it needs multiple rows. ' + 'Did you mean to pass multiple children or an array?' + '\n in SuspenseList (at **)' + @@ -188,7 +192,7 @@ describe('ReactSuspenseList', () => { it('warns if a nested array is passed to a "forwards" list', async () => { function Foo({items}) { return ( - + {items.map(name => ( {name} @@ -214,7 +218,7 @@ describe('ReactSuspenseList', () => { }); // @gate enableSuspenseList - it('shows content independently by default', async () => { + it('warns if no revealOrder is specified', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); @@ -250,6 +254,86 @@ describe('ReactSuspenseList', () => { 'Suspend! [C]', ]); + assertConsoleErrorDev([ + 'The default for the prop is changing. ' + + 'To be future compatible you must explictly specify either ' + + '"independent" (the current default), "together", "forwards" or "legacy_unstable-backwards".' + + '\n in SuspenseList (at **)' + + '\n in Foo (at **)', + ]); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + Loading B + Loading C + , + ); + + await act(() => C.resolve()); + assertLog( + gate('alwaysThrottleRetries') + ? ['Suspend! [B]', 'C', 'Suspend! [B]'] + : ['C'], + ); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + Loading B + C + , + ); + + await act(() => B.resolve()); + assertLog(['B']); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + C + , + ); + }); + + // @gate enableSuspenseList + it('shows content independently with revealOrder="independent"', async () => { + const A = createAsyncText('A'); + const B = createAsyncText('B'); + const C = createAsyncText('C'); + + function Foo() { + return ( + + }> +
+ + }> + + + }> + + + + ); + } + + await A.resolve(); + + ReactNoop.render(); + + await waitForAll([ + 'A', + 'Suspend! [B]', + 'Loading B', + 'Suspend! [C]', + 'Loading C', + // pre-warming + 'Suspend! [B]', + 'Suspend! [C]', + ]); + expect(ReactNoop).toMatchRenderedOutput( <> A @@ -564,7 +648,7 @@ describe('ReactSuspenseList', () => { }); // @gate enableSuspenseList - it('displays all "together" in nested SuspenseLists where the inner is default', async () => { + it('displays all "together" in nested SuspenseLists where the inner is "independent"', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); @@ -575,7 +659,7 @@ describe('ReactSuspenseList', () => { }> - + }> @@ -897,7 +981,7 @@ describe('ReactSuspenseList', () => { function Foo() { return ( - + }> @@ -955,6 +1039,85 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList + it('warns if revealOrder="backwards" is specified', async () => { + const A = createAsyncText('A'); + const B = createAsyncText('B'); + const C = createAsyncText('C'); + + function Foo() { + return ( + + }> + + + }> + + + }> + + + + ); + } + + await A.resolve(); + + ReactNoop.render(); + + await waitForAll([ + 'Suspend! [C]', + 'Loading C', + 'Loading B', + 'Loading A', + // pre-warming + 'Suspend! [C]', + ]); + + assertConsoleErrorDev([ + 'The rendering order of is changing. ' + + 'To be future compatible you must specify ' + + 'revealOrder="legacy_unstable-backwards" instead.' + + '\n in SuspenseList (at **)' + + '\n in Foo (at **)', + ]); + + expect(ReactNoop).toMatchRenderedOutput( + <> + Loading A + Loading B + Loading C + , + ); + + await act(() => C.resolve()); + assertLog([ + 'C', + 'Suspend! [B]', + // pre-warming + 'Suspend! [B]', + ]); + + expect(ReactNoop).toMatchRenderedOutput( + <> + Loading A + Loading B + C + , + ); + + await act(() => B.resolve()); + assertLog(['B', 'A']); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + C + , + ); + }); + // @gate enableSuspenseList it('displays each items in "backwards" order', async () => { const A = createAsyncText('A'); @@ -963,7 +1126,7 @@ describe('ReactSuspenseList', () => { function Foo() { return ( - + }> @@ -1037,7 +1200,7 @@ describe('ReactSuspenseList', () => { function Foo({items}) { return ( - + {items.map(([key, Component]) => ( }> @@ -1222,7 +1385,7 @@ describe('ReactSuspenseList', () => { function Foo({items}) { return ( - + {items.map(([key, Component]) => ( }> @@ -1400,7 +1563,7 @@ describe('ReactSuspenseList', () => { it('switches to rendering fallbacks if the tail takes long CPU time', async () => { function Foo() { return ( - + }> @@ -1535,6 +1698,29 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList + it('warns if no tail option is specified', async () => { + function Foo() { + return ( + + A + B + + ); + } + + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ + 'The default for the prop is changing. ' + + 'To be future compatible you must explictly specify either ' + + '"visible" (the current default), "collapsed" or "hidden".' + + '\n in SuspenseList (at **)' + + '\n in Foo (at **)', + ]); + }); + // @gate enableSuspenseList it('warns if an unsupported tail option is used', async () => { function Foo() { @@ -1551,7 +1737,7 @@ describe('ReactSuspenseList', () => { }); assertConsoleErrorDev([ '"collapse" is not a supported value for tail on ' + - '. Did you mean "collapsed" or "hidden"?' + + '. Did you mean "visible", "collapsed" or "hidden"?' + '\n in SuspenseList (at **)' + '\n in Foo (at **)', ]); @@ -1796,7 +1982,7 @@ describe('ReactSuspenseList', () => { function Foo({items}) { return ( - + {items.map(([key, Component]) => ( }> @@ -2154,7 +2340,7 @@ describe('ReactSuspenseList', () => { function Foo() { return ( - + }> @@ -2255,7 +2441,7 @@ describe('ReactSuspenseList', () => { function Foo({showB}) { return ( - + }> @@ -2321,7 +2507,7 @@ describe('ReactSuspenseList', () => { function Foo() { return (
- + @@ -2673,7 +2859,7 @@ describe('ReactSuspenseList', () => { function App() { Scheduler.log('App'); return ( - + }> @@ -2760,7 +2946,7 @@ describe('ReactSuspenseList', () => { Scheduler.log('App'); return ( - + }> @@ -2936,7 +3122,7 @@ describe('ReactSuspenseList', () => { // Several layers of Bailout wrappers help verify we're // marking updates all the way to the propagation root. return ( - + @@ -3029,7 +3215,7 @@ describe('ReactSuspenseList', () => { function Repro({update}) { return ( - + {update && ( }> @@ -3128,7 +3314,7 @@ describe('ReactSuspenseList', () => { } function Foo() { return ( - + ); @@ -3184,7 +3370,11 @@ describe('ReactSuspenseList', () => { }; function Foo() { - return {iterable}; + return ( + + {iterable} + + ); } await act(() => { @@ -3270,7 +3460,7 @@ describe('ReactSuspenseList', () => { it('warns if a nested async iterable is passed to a "forwards" list', async () => { function Foo({items}) { return ( - + {items}
Tail
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js index cad1a01181..2e252acbf3 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js @@ -345,7 +345,7 @@ describe('ReactSuspenseyCommitPhase', () => { it('demonstrate current behavior when used with SuspenseList (not ideal)', async () => { function App() { return ( - + }> diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index a18d8d1674..579edf25c9 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -1799,7 +1799,7 @@ function renderSuspenseListRows( task: Task, keyPath: KeyNode, rows: Array, - revealOrder: 'forwards' | 'backwards', + revealOrder: 'forwards' | 'backwards' | 'unstable_legacy-backwards', ): void { // This is a fork of renderChildrenArray that's aware of tracking rows. const prevKeyPath = task.keyPath; @@ -1827,7 +1827,11 @@ function renderSuspenseListRows( // 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 // render order. - const i = revealOrder !== 'backwards' ? n : totalChildren - 1 - n; + const i = + revealOrder !== 'backwards' && + revealOrder !== 'unstable_legacy-backwards' + ? n + : totalChildren - 1 - n; const node = rows[i]; task.row = previousSuspenseListRow = createSuspenseListRow( previousSuspenseListRow, @@ -1852,7 +1856,11 @@ function renderSuspenseListRows( // 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 // render order. - const i = revealOrder !== 'backwards' ? n : totalChildren - 1 - n; + const i = + revealOrder !== 'backwards' && + revealOrder !== 'unstable_legacy-backwards' + ? n + : totalChildren - 1 - n; const node = rows[i]; if (__DEV__) { warnForMissingKey(request, task, node); @@ -1869,7 +1877,10 @@ function renderSuspenseListRows( } } else { task = ((task: any): RenderTask); // Refined - if (revealOrder !== 'backwards') { + if ( + revealOrder !== 'backwards' && + revealOrder !== 'unstable_legacy-backwards' + ) { // Forwards direction for (let i = 0; i < totalChildren; i++) { const node = rows[i]; @@ -1973,7 +1984,11 @@ function renderSuspenseList( const revealOrder: SuspenseListRevealOrder = props.revealOrder; // TODO: Support tail hidden/collapsed modes. // const tailMode: SuspenseListTailMode = props.tail; - if (revealOrder === 'forwards' || revealOrder === 'backwards') { + if ( + revealOrder === 'forwards' || + revealOrder === 'backwards' || + revealOrder === 'unstable_legacy-backwards' + ) { // For ordered reveal, we need to produce rows from the children. if (isArray(children)) { renderSuspenseListRows(request, task, keyPath, children, revealOrder); diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index b2b2f25fba..ea9d3d1d07 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -308,20 +308,32 @@ export type SuspenseProps = { export type SuspenseListRevealOrder = | 'forwards' | 'backwards' + | 'unstable_legacy-backwards' | 'together' + | 'independent' | void; -export type SuspenseListTailMode = 'collapsed' | 'hidden' | void; +export type SuspenseListTailMode = 'visible' | 'collapsed' | 'hidden' | void; + +// A SuspenseList row cannot include a nested Array since it's an easy mistake to not realize it +// is treated as a single row. A Fragment can be used to intentionally have multiple children as +// a single row. +type SuspenseListRow = Exclude< + ReactNodeList, + Iterable | AsyncIterable, +>; type DirectionalSuspenseListProps = { - children?: ReactNodeList, - revealOrder: 'forwards' | 'backwards', + // Directional SuspenseList are defined by an array of children or multiple slots to JSX + // It does not allow a single element child. + children?: Iterable | AsyncIterable, // Note: AsyncIterable is experimental. + revealOrder: 'forwards' | 'backwards' | 'unstable_legacy-backwards', tail?: SuspenseListTailMode, }; type NonDirectionalSuspenseListProps = { children?: ReactNodeList, - revealOrder?: 'together' | void, + revealOrder?: 'independent' | 'together' | void, tail?: void, };