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,