Skip to content

Replace Implicit Options on SuspenseList with Explicit Options #33424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ module.exports = {
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',
Expand Down
2 changes: 1 addition & 1 deletion fixtures/ssr/src/components/LargeContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,7 @@ describe('ReactDOMFizzServer', () => {
function App({showMore}) {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{a}
{b}
{showMore ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2254,7 +2254,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
function App() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback="Loading A">
<ComponentA />
</Suspense>
Expand Down
84 changes: 75 additions & 9 deletions packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -586,15 +650,15 @@ 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');

function Foo() {
return (
<div>
<SuspenseList revealOrder="backwards">
<SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2362,7 +2362,7 @@ describe('ReactDOMServerPartialHydration', () => {

function App({showMore}) {
return (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
{a}
{b}
{showMore ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 52 additions & 16 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down Expand Up @@ -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.',
Expand All @@ -3259,15 +3272,15 @@ 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;
}
} 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,
);
}
Expand All @@ -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". ' +
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions packages/react-reconciler/src/ReactFiberSuspenseComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ describe('ReactLazyContextPropagation', () => {
setContext = setValue;
const children = React.useMemo(
() => (
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="forwards" tail="visible">
<Child />
<Child />
</SuspenseList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ describe('ReactFragment', () => {
onCaughtError,
}).render(
<CatchingBoundary>
<SuspenseList>
<SuspenseList revealOrder="independent">
<SomethingThatErrors />
</SuspenseList>
</CatchingBoundary>,
Expand Down
Loading
Loading