From 26cf2804802f3d32c4d8f9db73ddea12ad6c1670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 31 Oct 2025 12:58:18 -0400 Subject: [PATCH 1/2] Switch the default revealOrder to "forwards" and tail "hidden" on SuspenseList (#35018) We have warned about this for a while now so we can make the switch. Often when you reach for SuspenseList, you mean forwards. It doesn't make sense to have the default to just be a noop. While "together" is another useful mode that's more like a Group so isn't so associated with the default as List. So we're switching it. However, tail=hidden isn't as obvious of a default it does allow for a convenient pattern for streaming in list of items by default. This doesn't yet switch the rendering order of "backwards". That's coming in a follow up. --- .../ReactDOMFizzSuspenseList-test.js | 21 ++- .../react-reconciler/src/ReactChildFiber.js | 3 +- .../src/ReactFiberBeginWork.js | 80 +++++------- .../src/ReactFiberCompleteWork.js | 29 +++-- .../src/ReactFiberSuspenseComponent.js | 5 +- .../src/__tests__/ReactSuspenseList-test.js | 121 ++++++++++-------- packages/react-server/src/ReactFizzServer.js | 8 +- 7 files changed, 142 insertions(+), 125 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js index 74db51d2429..f434bf3cda8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js @@ -134,7 +134,7 @@ describe('ReactDOMFizzSuspenseList', () => { } // @gate enableSuspenseList - it('shows content independently by default', async () => { + it('shows content forwards by default', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); @@ -157,31 +157,38 @@ describe('ReactDOMFizzSuspenseList', () => { ); } - await A.resolve(); + await C.resolve(); await serverAct(async () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); pipe(writable); }); - assertLog(['A', 'Suspend! [B]', 'Suspend! [C]', 'Loading B', 'Loading C']); + assertLog([ + 'Suspend! [A]', + 'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended, + 'C', + 'Loading A', + 'Loading B', + 'Loading C', + ]); expect(getVisibleChildren(container)).toEqual(
- A + Loading A Loading B Loading C
, ); - await serverAct(() => C.resolve()); - assertLog(['C']); + await serverAct(() => A.resolve()); + assertLog(['A']); expect(getVisibleChildren(container)).toEqual(
A Loading B - C + Loading C
, ); diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index f0d9c2e012e..8daf7fde4b4 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -2104,7 +2104,8 @@ export function validateSuspenseListChildren( ) { if (__DEV__) { if ( - (revealOrder === 'forwards' || + (revealOrder == null || + revealOrder === 'forwards' || revealOrder === 'backwards' || revealOrder === 'unstable_legacy-backwards') && children !== undefined && diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 7251e52dd61..32ecd33edca 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -3245,6 +3245,7 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { if (__DEV__) { const cacheKey = revealOrder == null ? 'null' : revealOrder; if ( + revealOrder != null && revealOrder !== 'forwards' && revealOrder !== 'unstable_legacy-backwards' && revealOrder !== 'together' && @@ -3252,13 +3253,7 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { !didWarnAboutRevealOrder[cacheKey] ) { 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') { + if (revealOrder === 'backwards') { console.error( 'The rendering order of is changing. ' + 'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.', @@ -3314,18 +3309,7 @@ function validateTailOptions( 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".', - ); - } + // The default tail is now "hidden". } else if ( tailMode !== 'visible' && tailMode !== 'collapsed' && @@ -3338,6 +3322,7 @@ function validateTailOptions( tailMode, ); } else if ( + revealOrder != null && revealOrder !== 'forwards' && revealOrder !== 'backwards' && revealOrder !== 'unstable_legacy-backwards' @@ -3345,7 +3330,7 @@ function validateTailOptions( didWarnAboutTailOptions[cacheKey] = true; console.error( ' is only valid if revealOrder is ' + - '"forwards" or "backwards". ' + + '"forwards" (default) or "backwards". ' + 'Did you mean to specify revealOrder="forwards"?', tailMode, ); @@ -3449,30 +3434,6 @@ function updateSuspenseListComponent( workInProgress.memoizedState = null; } else { switch (revealOrder) { - case 'forwards': { - const lastContentRow = findLastContentRow(workInProgress.child); - let tail; - if (lastContentRow === null) { - // The whole list is part of the tail. - // TODO: We could fast path by just rendering the tail now. - tail = workInProgress.child; - workInProgress.child = null; - } else { - // Disconnect the tail rows after the content row. - // We're going to render them separately later. - tail = lastContentRow.sibling; - lastContentRow.sibling = null; - } - initSuspenseListRenderState( - workInProgress, - false, // isBackwards - tail, - lastContentRow, - tailMode, - treeForkCount, - ); - break; - } case 'backwards': case 'unstable_legacy-backwards': { // We're going to find the first row that has existing content. @@ -3517,10 +3478,37 @@ function updateSuspenseListComponent( ); break; } - default: { - // The default reveal order is the same as not having + case 'independent': { + // The "independent" reveal order is the same as not having // a boundary. workInProgress.memoizedState = null; + break; + } + // The default is now forwards. + case 'forwards': + default: { + const lastContentRow = findLastContentRow(workInProgress.child); + let tail; + if (lastContentRow === null) { + // The whole list is part of the tail. + // TODO: We could fast path by just rendering the tail now. + tail = workInProgress.child; + workInProgress.child = null; + } else { + // Disconnect the tail rows after the content row. + // We're going to render them separately later. + tail = lastContentRow.sibling; + lastContentRow.sibling = null; + } + initSuspenseListRenderState( + workInProgress, + false, // isBackwards + tail, + lastContentRow, + tailMode, + treeForkCount, + ); + break; } } } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index d3abcb466e5..5ccf1b2ac7a 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -698,7 +698,11 @@ function cutOffTailIfNeeded( return; } switch (renderState.tailMode) { - case 'hidden': { + case 'visible': { + // Everything should remain as it was. + break; + } + case 'collapsed': { // Any insertions at the end of the tail list after this point // should be invisible. If there are already mounted boundaries // anything before them are not considered for collapsing. @@ -716,7 +720,13 @@ function cutOffTailIfNeeded( // last rendered item. if (lastTailNode === null) { // All remaining items in the tail are insertions. - renderState.tail = null; + if (!hasRenderedATailFallback && renderState.tail !== null) { + // We suspended during the head. We want to show at least one + // row at the tail. So we'll keep on and cut off the rest. + renderState.tail.sibling = null; + } else { + renderState.tail = null; + } } else { // Detach the insertion after the last node that was already // inserted. @@ -724,7 +734,9 @@ function cutOffTailIfNeeded( } break; } - case 'collapsed': { + // Hidden is now the default. + case 'hidden': + default: { // Any insertions at the end of the tail list after this point // should be invisible. If there are already mounted boundaries // anything before them are not considered for collapsing. @@ -742,13 +754,7 @@ function cutOffTailIfNeeded( // last rendered item. if (lastTailNode === null) { // All remaining items in the tail are insertions. - if (!hasRenderedATailFallback && renderState.tail !== null) { - // We suspended during the head. We want to show at least one - // row at the tail. So we'll keep on and cut off the rest. - renderState.tail.sibling = null; - } else { - renderState.tail = null; - } + renderState.tail = null; } else { // Detach the insertion after the last node that was already // inserted. @@ -1795,7 +1801,8 @@ function completeWork( // This might have been modified. if ( renderState.tail === null && - renderState.tailMode === 'hidden' && + renderState.tailMode !== 'collapsed' && + renderState.tailMode !== 'visible' && !renderedTail.alternate && !getIsHydrating() // We don't cut it if we're hydrating. ) { diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index 64ab4f29fcd..695c3697154 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -79,10 +79,7 @@ export function findFirstSuspended(row: Fiber): null | Fiber { node.tag === SuspenseListComponent && // Independent revealOrder can't be trusted because it doesn't // keep track of whether it suspended or not. - (node.memoizedProps.revealOrder === 'forwards' || - node.memoizedProps.revealOrder === 'backwards' || - node.memoizedProps.revealOrder === 'unstable_legacy-backwards' || - node.memoizedProps.revealOrder === 'together') + node.memoizedProps.revealOrder !== 'independent' ) { const didSuspend = (node.flags & DidCapture) !== NoFlags; if (didSuspend) { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index bfbf165dbab..34600b7a672 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -218,7 +218,7 @@ describe('ReactSuspenseList', () => { }); // @gate enableSuspenseList - it('warns if no revealOrder is specified', async () => { + it('behaves as revealOrder=forwards by default', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); @@ -239,54 +239,36 @@ describe('ReactSuspenseList', () => { ); } + ReactNoop.render(); + + await waitForAll(['Suspend! [A]', 'Loading A']); + + expect(ReactNoop).toMatchRenderedOutput(null); + await A.resolve(); - ReactNoop.render(); + await waitForAll(['A', 'Suspend! [B]', 'Loading B']); - await waitForAll([ - 'A', - 'Suspend! [B]', - 'Loading B', - 'Suspend! [C]', - 'Loading C', - // pre-warming - 'Suspend! [B]', - 'Suspend! [C]', - ]); + // Incremental loading is suspended. + jest.advanceTimersByTime(500); - 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); - expect(ReactNoop).toMatchRenderedOutput( - <> - A - Loading B - Loading C - , - ); + await act(() => B.resolve()); + assertLog(['B', 'Suspend! [C]', 'Loading C']); - await act(() => C.resolve()); - assertLog( - gate('alwaysThrottleRetries') - ? ['Suspend! [B]', 'C', 'Suspend! [B]'] - : ['C'], - ); + // Incremental loading is suspended. + jest.advanceTimersByTime(500); expect(ReactNoop).toMatchRenderedOutput( <> A - Loading B - C + B , ); - await act(() => B.resolve()); - assertLog(['B']); + await act(() => C.resolve()); + assertLog(['C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -1699,26 +1681,65 @@ describe('ReactSuspenseList', () => { }); // @gate enableSuspenseList - it('warns if no tail option is specified', async () => { + it('behaves as tail=hidden if no tail option is specified', async () => { + const A = createAsyncText('A'); + const B = createAsyncText('B'); + const C = createAsyncText('C'); + 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 **)', - ]); + ReactNoop.render(); + + await waitForAll(['Suspend! [A]', 'Loading A']); + + expect(ReactNoop).toMatchRenderedOutput(null); + + await A.resolve(); + + await waitForAll(['A', 'Suspend! [B]', 'Loading B']); + + // Incremental loading is suspended. + jest.advanceTimersByTime(500); + + expect(ReactNoop).toMatchRenderedOutput(A); + + await act(() => B.resolve()); + assertLog(['B', 'Suspend! [C]', 'Loading C']); + + // Incremental loading is suspended. + jest.advanceTimersByTime(500); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + , + ); + + await act(() => C.resolve()); + assertLog(['C']); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + C + , + ); }); // @gate enableSuspenseList @@ -1758,7 +1779,7 @@ describe('ReactSuspenseList', () => { }); assertConsoleErrorDev([ ' is only valid if ' + - 'revealOrder is "forwards" or "backwards". ' + + 'revealOrder is "forwards" (default) or "backwards". ' + 'Did you mean to specify revealOrder="forwards"?' + '\n in SuspenseList (at **)' + '\n in Foo (at **)', diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 07408d64e88..156bacbe148 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -1913,7 +1913,7 @@ function renderSuspenseListRows( task: Task, keyPath: KeyNode, rows: Array, - revealOrder: 'forwards' | 'backwards' | 'unstable_legacy-backwards', + revealOrder: void | 'forwards' | 'backwards' | 'unstable_legacy-backwards', ): void { // This is a fork of renderChildrenArray that's aware of tracking rows. const prevKeyPath = task.keyPath; @@ -2098,11 +2098,7 @@ function renderSuspenseList( const revealOrder: SuspenseListRevealOrder = props.revealOrder; // TODO: Support tail hidden/collapsed modes. // const tailMode: SuspenseListTailMode = props.tail; - if ( - revealOrder === 'forwards' || - revealOrder === 'backwards' || - revealOrder === 'unstable_legacy-backwards' - ) { + if (revealOrder !== 'independent' && revealOrder !== 'together') { // For ordered reveal, we need to produce rows from the children. if (isArray(children)) { renderSuspenseListRows(request, task, keyPath, children, revealOrder); From 488d88b018ee8fd1fac56cab22dfa8796ebce30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 31 Oct 2025 13:33:23 -0400 Subject: [PATCH 2/2] Render children passed to "backwards" SuspenseList in reverse mount order (#35021) Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. image --- .../ReactDOMFizzSuspenseList-test.js | 81 +++++++++++++++++-- .../src/ReactFiberBeginWork.js | 62 ++++++++++++-- .../src/ReactFiberCompleteWork.js | 4 - .../src/__tests__/ReactSuspenseList-test.js | 20 ++--- packages/react-server/src/ReactFizzServer.js | 6 +- 5 files changed, 140 insertions(+), 33 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js index f434bf3cda8..0e2fff5cd45 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js @@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => { ); }); + // @gate enableSuspenseList + it('displays each items in "backwards" mount order', 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([ + 'Suspend! [C]', + 'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended, + 'A', + 'Loading C', + 'Loading B', + 'Loading A', + ]); + + expect(getVisibleChildren(container)).toEqual( +
+ Loading A + Loading B + Loading C +
, + ); + + await serverAct(() => C.resolve()); + assertLog(['C']); + + expect(getVisibleChildren(container)).toEqual( +
+ Loading A + Loading B + C +
, + ); + + await serverAct(() => B.resolve()); + assertLog(['B']); + + expect(getVisibleChildren(container)).toEqual( +
+ A + B + C +
, + ); + }); + // @gate enableSuspenseList it('displays each items in "backwards" order in legacy mode', async () => { const A = createAsyncText('A'); @@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => { return (
- - }> - - + }> + }> + + }> diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 32ecd33edca..33fadfd5396 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { if ( revealOrder != null && revealOrder !== 'forwards' && + revealOrder !== 'backwards' && revealOrder !== 'unstable_legacy-backwards' && revealOrder !== 'together' && revealOrder !== 'independent' && !didWarnAboutRevealOrder[cacheKey] ) { didWarnAboutRevealOrder[cacheKey] = true; - 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') { + if (typeof revealOrder === 'string') { switch (revealOrder.toLowerCase()) { case 'together': case 'forwards': @@ -3371,6 +3367,17 @@ function initSuspenseListRenderState( } } +function reverseChildren(fiber: Fiber): void { + let row = fiber.child; + fiber.child = null; + while (row !== null) { + const nextRow = row.sibling; + row.sibling = fiber.child; + fiber.child = row; + row = nextRow; + } +} + // This can end up rendering this component multiple passes. // The first pass splits the children fibers into two sets. A head and tail. // We first render the head. If anything is in fallback state, we do another @@ -3409,7 +3416,16 @@ function updateSuspenseListComponent( validateTailOptions(tailMode, revealOrder); validateSuspenseListChildren(newChildren, revealOrder); - reconcileChildren(current, workInProgress, newChildren, renderLanes); + if (revealOrder === 'backwards' && current !== null) { + // For backwards the current mounted set will be backwards. Reconciling against it + // will lead to mismatches and reorders. We need to swap the original set first + // and then restore it afterwards. + reverseChildren(current); + reconcileChildren(current, workInProgress, newChildren, renderLanes); + reverseChildren(current); + } else { + reconcileChildren(current, workInProgress, newChildren, renderLanes); + } // Read how many children forks this set pushed so we can push it every time we retry. const treeForkCount = getIsHydrating() ? getForksAtLevel(workInProgress) : 0; @@ -3434,7 +3450,37 @@ function updateSuspenseListComponent( workInProgress.memoizedState = null; } else { switch (revealOrder) { - case 'backwards': + case 'backwards': { + // We're going to find the first row that has existing content. + // We are also going to reverse the order of anything in the existing content + // since we want to actually render them backwards from the reconciled set. + // The tail is left in order, because it'll be added to the front as we + // complete each item. + const lastContentRow = findLastContentRow(workInProgress.child); + let tail; + if (lastContentRow === null) { + // The whole list is part of the tail. + tail = workInProgress.child; + workInProgress.child = null; + } else { + // Disconnect the tail rows after the content row. + // We're going to render them separately later in reverse order. + tail = lastContentRow.sibling; + lastContentRow.sibling = null; + // We have to now reverse the main content so it renders backwards too. + reverseChildren(workInProgress); + } + // TODO: If workInProgress.child is null, we can continue on the tail immediately. + initSuspenseListRenderState( + workInProgress, + true, // isBackwards + tail, + null, // last + tailMode, + treeForkCount, + ); + break; + } 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 diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 5ccf1b2ac7a..0b1f1e4366e 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -1838,10 +1838,6 @@ function completeWork( } } if (renderState.isBackwards) { - // The effect list of the backwards tail will have been added - // to the end. This breaks the guarantee that life-cycles fire in - // sibling order but that isn't a strong guarantee promised by React. - // Especially since these might also just pop in during future commits. // Append to the beginning of the list. renderedTail.sibling = workInProgress.child; workInProgress.child = renderedTail; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index 34600b7a672..ba5b668652f 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -1022,7 +1022,7 @@ describe('ReactSuspenseList', () => { }); // @gate enableSuspenseList - it('warns if revealOrder="backwards" is specified', async () => { + it('displays each items in "backwards" order', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); @@ -1030,14 +1030,14 @@ describe('ReactSuspenseList', () => { function Foo() { return ( - }> - + }> + }> - }> - + }> + ); @@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => { '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 @@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => { }); // @gate enableSuspenseList - it('displays each items in "backwards" order', async () => { + it('displays each items in "backwards" order (legacy)', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); const C = createAsyncText('C'); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 156bacbe148..33c0ab68a8e 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -2017,7 +2017,11 @@ function renderSuspenseListRows( const parentSegment = task.blockedSegment; const childIndex = parentSegment.children.length; const insertionIndex = parentSegment.chunks.length; - for (let i = totalChildren - 1; i >= 0; i--) { + for (let n = 0; n < totalChildren; n++) { + const i = + revealOrder === 'unstable_legacy-backwards' + ? totalChildren - 1 - n + : n; const node = rows[i]; task.row = previousSuspenseListRow = createSuspenseListRow( previousSuspenseListRow,