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.
This commit is contained in:
Sebastian Markbåge 2025-06-03 17:40:30 -04:00 committed by GitHub
parent 1540081725
commit d742611ce4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 397 additions and 72 deletions

View File

@ -611,6 +611,7 @@ module.exports = {
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',

View File

@ -6,7 +6,7 @@ import React, {
export default function LargeContent() {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={null}>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris

View File

@ -1289,7 +1289,7 @@ describe('ReactDOMFizzServer', () => {
function App({showMore}) {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{a}
{b}
{showMore ? (

View File

@ -2254,7 +2254,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
function App() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback="Loading A">
<ComponentA />
</Suspense>

View File

@ -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 (
<div>
<SuspenseList revealOrder="independent">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
</div>
);
}
await A.resolve();
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
assertLog(['A', 'Suspend! [B]', 'Suspend! [C]', 'Loading B', 'Loading C']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);
await serverAct(() => C.resolve());
assertLog(['C']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>Loading B</span>
<span>C</span>
</div>,
);
await serverAct(() => B.resolve());
assertLog(['B']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B</span>
<span>C</span>
</div>,
);
});
// @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', () => {
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<SuspenseList>
<SuspenseList revealOrder="independent">
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
@ -523,7 +587,7 @@ describe('ReactDOMFizzSuspenseList', () => {
function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
@ -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 (
<div>
<SuspenseList revealOrder="backwards">
<SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
@ -665,8 +729,10 @@ describe('ReactDOMFizzSuspenseList', () => {
function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="backwards">
<SuspenseList revealOrder="forwards" tail="visible">
<SuspenseList
revealOrder="unstable_legacy-backwards"
tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
@ -736,7 +802,7 @@ describe('ReactDOMFizzSuspenseList', () => {
function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
@ -791,7 +857,7 @@ describe('ReactDOMFizzSuspenseList', () => {
function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>

View File

@ -5755,7 +5755,7 @@ body {
<html>
<body>
<Suspense fallback="loading...">
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback="loading foo...">
<BlockedOn value="foo">
<link rel="stylesheet" href="foo" precedence="foo" />

View File

@ -2362,7 +2362,7 @@ describe('ReactDOMServerPartialHydration', () => {
function App({showMore}) {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{a}
{b}
{showMore ? (

View File

@ -123,7 +123,7 @@ describe('ReactDOMServerSuspense', () => {
// @gate enableSuspenseList
it('server renders a SuspenseList component and its children', async () => {
const example = (
<SuspenseList>
<SuspenseList revealOrder="forwards" tail="visible">
<React.Suspense fallback="Loading A">
<div>A</div>
</React.Suspense>

View File

@ -172,7 +172,7 @@ test('regression (#20932): return pointer is correct before entering deleted tre
function App() {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading Async..." />}>
<Async />
</Suspense>

View File

@ -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

View File

@ -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 <SuspenseList revealOrder="..."> 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 <SuspenseList revealOrder="backwards"> 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 <SuspenseList />. ' +
'Use lowercase "%s" instead.',
@ -3259,7 +3272,7 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
default:
console.error(
'"%s" is not a supported revealOrder on <SuspenseList />. ' +
'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 <SuspenseList />. ' +
'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 <SuspenseList tail="..."> 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 <SuspenseList />. ' +
'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(
'<SuspenseList tail="%s" /> 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

View File

@ -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) {

View File

@ -677,7 +677,7 @@ describe('ReactLazyContextPropagation', () => {
setContext = setValue;
const children = React.useMemo(
() => (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Child />
<Child />
</SuspenseList>

View File

@ -255,7 +255,7 @@ describe('ReactFragment', () => {
onCaughtError,
}).render(
<CatchingBoundary>
<SuspenseList>
<SuspenseList revealOrder="independent">
<SomethingThatErrors />
</SuspenseList>
</CatchingBoundary>,

View File

@ -79,7 +79,7 @@ describe('ReactSuspenseList', () => {
});
assertConsoleErrorDev([
'"something" is not a supported revealOrder on ' +
'<SuspenseList />. Did you mean "together", "forwards" or "backwards"?' +
'<SuspenseList />. 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 <SuspenseList revealOrder="forwards">{children}</SuspenseList>;
return (
<SuspenseList revealOrder="forwards" tail="visible">
{children}
</SuspenseList>
);
}
ReactNoop.render(<Foo />);
@ -166,7 +170,7 @@ describe('ReactSuspenseList', () => {
it('warns if a single fragment is passed to a "backwards" list', async () => {
function Foo() {
return (
<SuspenseList revealOrder="backwards">
<SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
<>{[]}</>
</SuspenseList>
);
@ -176,7 +180,7 @@ describe('ReactSuspenseList', () => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'A single row was passed to a <SuspenseList revealOrder="backwards" />. ' +
'A single row was passed to a <SuspenseList revealOrder="unstable_legacy-backwards" />. ' +
'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 (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{items.map(name => (
<Suspense key={name} fallback="Loading">
{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 <SuspenseList revealOrder="..."> 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(
<>
<span>A</span>
<span>Loading B</span>
<span>Loading C</span>
</>,
);
await act(() => C.resolve());
assertLog(
gate('alwaysThrottleRetries')
? ['Suspend! [B]', 'C', 'Suspend! [B]']
: ['C'],
);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>Loading B</span>
<span>C</span>
</>,
);
await act(() => B.resolve());
assertLog(['B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
<span>C</span>
</>,
);
});
// @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 (
<SuspenseList revealOrder="independent">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
);
}
await A.resolve();
ReactNoop.render(<Foo />);
await waitForAll([
'A',
'Suspend! [B]',
'Loading B',
'Suspend! [C]',
'Loading C',
// pre-warming
'Suspend! [B]',
'Suspend! [C]',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
@ -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', () => {
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<SuspenseList>
<SuspenseList revealOrder="independent">
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
@ -897,7 +981,7 @@ describe('ReactSuspenseList', () => {
function Foo() {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
@ -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 (
<SuspenseList revealOrder="backwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
);
}
await A.resolve();
ReactNoop.render(<Foo />);
await waitForAll([
'Suspend! [C]',
'Loading C',
'Loading B',
'Loading A',
// pre-warming
'Suspend! [C]',
]);
assertConsoleErrorDev([
'The rendering order of <SuspenseList revealOrder="backwards"> 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(
<>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</>,
);
await act(() => C.resolve());
assertLog([
'C',
'Suspend! [B]',
// pre-warming
'Suspend! [B]',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>Loading A</span>
<span>Loading B</span>
<span>C</span>
</>,
);
await act(() => B.resolve());
assertLog(['B', 'A']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
<span>C</span>
</>,
);
});
// @gate enableSuspenseList
it('displays each items in "backwards" order', async () => {
const A = createAsyncText('A');
@ -963,7 +1126,7 @@ describe('ReactSuspenseList', () => {
function Foo() {
return (
<SuspenseList revealOrder="backwards">
<SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
@ -1037,7 +1200,7 @@ describe('ReactSuspenseList', () => {
function Foo({items}) {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{items.map(([key, Component]) => (
<Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
<Component />
@ -1222,7 +1385,7 @@ describe('ReactSuspenseList', () => {
function Foo({items}) {
return (
<SuspenseList revealOrder="backwards">
<SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
{items.map(([key, Component]) => (
<Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
<Component />
@ -1400,7 +1563,7 @@ describe('ReactSuspenseList', () => {
it('switches to rendering fallbacks if the tail takes long CPU time', async () => {
function Foo() {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<Text text="A" />
</Suspense>
@ -1535,6 +1698,29 @@ describe('ReactSuspenseList', () => {
);
});
// @gate enableSuspenseList
it('warns if no tail option is specified', async () => {
function Foo() {
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback="Loading">A</Suspense>
<Suspense fallback="Loading">B</Suspense>
</SuspenseList>
);
}
await act(() => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'The default for the <SuspenseList tail="..."> 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 ' +
'<SuspenseList />. Did you mean "collapsed" or "hidden"?' +
'<SuspenseList />. 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 (
<SuspenseList revealOrder="backwards" tail="collapsed">
<SuspenseList revealOrder="unstable_legacy-backwards" tail="collapsed">
{items.map(([key, Component]) => (
<Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
<Component />
@ -2154,7 +2340,7 @@ describe('ReactSuspenseList', () => {
function Foo() {
return (
<SuspenseList revealOrder="together">
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<Text text="A" />
</Suspense>
@ -2255,7 +2441,7 @@ describe('ReactSuspenseList', () => {
function Foo({showB}) {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<SuspenseList revealOrder="forwards" tail="hidden">
<Suspense fallback={<Text text="Loading A" />}>
<Text text="A" />
@ -2321,7 +2507,7 @@ describe('ReactSuspenseList', () => {
function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Text text="A" />
<Text text="B" />
</SuspenseList>
@ -2673,7 +2859,7 @@ describe('ReactSuspenseList', () => {
function App() {
Scheduler.log('App');
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<Sleep time={600}>
<TwoPass text="A" />
@ -2760,7 +2946,7 @@ describe('ReactSuspenseList', () => {
Scheduler.log('App');
return (
<Profiler id="root" onRender={onRender}>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Fallback />}>
<Sleep time={1}>
<A />
@ -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 (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Bailout>
<Bailout>
<Bailout>
@ -3029,7 +3215,7 @@ describe('ReactSuspenseList', () => {
function Repro({update}) {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{update && (
<Suspense fallback={<Text text="Loading A..." />}>
<A />
@ -3128,7 +3314,7 @@ describe('ReactSuspenseList', () => {
}
function Foo() {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Generator />
</SuspenseList>
);
@ -3184,7 +3370,11 @@ describe('ReactSuspenseList', () => {
};
function Foo() {
return <SuspenseList revealOrder="forwards">{iterable}</SuspenseList>;
return (
<SuspenseList revealOrder="forwards" tail="visible">
{iterable}
</SuspenseList>
);
}
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 (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{items}
<div>Tail</div>
</SuspenseList>

View File

@ -345,7 +345,7 @@ describe('ReactSuspenseyCommitPhase', () => {
it('demonstrate current behavior when used with SuspenseList (not ideal)', async () => {
function App() {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<SuspenseyImage src="A" />
</Suspense>

View File

@ -1799,7 +1799,7 @@ function renderSuspenseListRows(
task: Task,
keyPath: KeyNode,
rows: Array<ReactNodeList>,
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);

View File

@ -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<React$Node> | AsyncIterable<React$Node>,
>;
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<SuspenseListRow> | AsyncIterable<SuspenseListRow>, // 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,
};