diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
index f434bf3cda8ec..0e2fff5cd4510 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 32ecd33edca65..33fadfd539668 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 5ccf1b2ac7ac8..0b1f1e4366e79 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 34600b7a67283..ba5b668652f9a 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 156bacbe1480c..33c0ab68a8e10 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,