mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Some checks failed
(Compiler) Playground / Test playground (push) Has been cancelled
(Compiler) TypeScript / Discover yarn workspaces (push) Has been cancelled
(Compiler) TypeScript / Lint babel-plugin-react-compiler (push) Has been cancelled
(Compiler) TypeScript / Jest babel-plugin-react-compiler (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (6) (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (7) (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (8) (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (9) (push) Has been cancelled
(Runtime) Fuzz tests / test_fuzz (push) Has been cancelled
(Shared) Lint / Run prettier (push) Has been cancelled
(Shared) Lint / Run eslint (push) Has been cancelled
(Shared) Lint / Check license (push) Has been cancelled
(Shared) Lint / Test print warnings (push) Has been cancelled
(Compiler) TypeScript / Test ${{ matrix.workspace_name }} (push) Has been cancelled
(Runtime) Publish Prereleases Nightly / Publish to Canary channel (push) Has been cancelled
(Compiler) Publish Prereleases Nightly / Publish to Experimental channel (push) Has been cancelled
(Runtime) Publish Prereleases Nightly / Publish to Experimental channel (push) Has been cancelled
Some checks failed
(Compiler) Playground / Test playground (push) Has been cancelled
(Compiler) TypeScript / Discover yarn workspaces (push) Has been cancelled
(Compiler) TypeScript / Lint babel-plugin-react-compiler (push) Has been cancelled
(Compiler) TypeScript / Jest babel-plugin-react-compiler (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (6) (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (7) (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (8) (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v${{ matrix.eslint_major }} (9) (push) Has been cancelled
(Runtime) Fuzz tests / test_fuzz (push) Has been cancelled
(Shared) Lint / Run prettier (push) Has been cancelled
(Shared) Lint / Run eslint (push) Has been cancelled
(Shared) Lint / Check license (push) Has been cancelled
(Shared) Lint / Test print warnings (push) Has been cancelled
(Compiler) TypeScript / Test ${{ matrix.workspace_name }} (push) Has been cancelled
(Runtime) Publish Prereleases Nightly / Publish to Canary channel (push) Has been cancelled
(Compiler) Publish Prereleases Nightly / Publish to Experimental channel (push) Has been cancelled
(Runtime) Publish Prereleases Nightly / Publish to Experimental channel (push) Has been cancelled
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. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
This commit is contained in:
parent
26cf280480
commit
488d88b018
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<SuspenseList revealOrder="backwards" tail="visible">
|
||||||
|
<Suspense fallback={<Text text="Loading C" />}>
|
||||||
|
<C />
|
||||||
|
</Suspense>
|
||||||
|
<Suspense fallback={<Text text="Loading B" />}>
|
||||||
|
<B />
|
||||||
|
</Suspense>
|
||||||
|
<Suspense fallback={<Text text="Loading A" />}>
|
||||||
|
<A />
|
||||||
|
</Suspense>
|
||||||
|
</SuspenseList>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await A.resolve();
|
||||||
|
|
||||||
|
await serverAct(async () => {
|
||||||
|
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
|
||||||
|
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(
|
||||||
|
<div>
|
||||||
|
<span>Loading A</span>
|
||||||
|
<span>Loading B</span>
|
||||||
|
<span>Loading C</span>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await serverAct(() => C.resolve());
|
||||||
|
assertLog(['C']);
|
||||||
|
|
||||||
|
expect(getVisibleChildren(container)).toEqual(
|
||||||
|
<div>
|
||||||
|
<span>Loading 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
|
// @gate enableSuspenseList
|
||||||
it('displays each items in "backwards" order in legacy mode', async () => {
|
it('displays each items in "backwards" order in legacy mode', async () => {
|
||||||
const A = createAsyncText('A');
|
const A = createAsyncText('A');
|
||||||
|
|
@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SuspenseList revealOrder="forwards" tail="visible">
|
<SuspenseList revealOrder="forwards" tail="visible">
|
||||||
<SuspenseList
|
<SuspenseList revealOrder="backwards" tail="visible">
|
||||||
revealOrder="unstable_legacy-backwards"
|
|
||||||
tail="visible">
|
|
||||||
<Suspense fallback={<Text text="Loading A" />}>
|
|
||||||
<A />
|
|
||||||
</Suspense>
|
|
||||||
<Suspense fallback={<Text text="Loading B" />}>
|
<Suspense fallback={<Text text="Loading B" />}>
|
||||||
<B />
|
<B />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<Suspense fallback={<Text text="Loading A" />}>
|
||||||
|
<A />
|
||||||
|
</Suspense>
|
||||||
</SuspenseList>
|
</SuspenseList>
|
||||||
<Suspense fallback={<Text text="Loading C" />}>
|
<Suspense fallback={<Text text="Loading C" />}>
|
||||||
<C />
|
<C />
|
||||||
|
|
|
||||||
|
|
@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
|
||||||
if (
|
if (
|
||||||
revealOrder != null &&
|
revealOrder != null &&
|
||||||
revealOrder !== 'forwards' &&
|
revealOrder !== 'forwards' &&
|
||||||
|
revealOrder !== 'backwards' &&
|
||||||
revealOrder !== 'unstable_legacy-backwards' &&
|
revealOrder !== 'unstable_legacy-backwards' &&
|
||||||
revealOrder !== 'together' &&
|
revealOrder !== 'together' &&
|
||||||
revealOrder !== 'independent' &&
|
revealOrder !== 'independent' &&
|
||||||
!didWarnAboutRevealOrder[cacheKey]
|
!didWarnAboutRevealOrder[cacheKey]
|
||||||
) {
|
) {
|
||||||
didWarnAboutRevealOrder[cacheKey] = true;
|
didWarnAboutRevealOrder[cacheKey] = true;
|
||||||
if (revealOrder === 'backwards') {
|
if (typeof revealOrder === 'string') {
|
||||||
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()) {
|
switch (revealOrder.toLowerCase()) {
|
||||||
case 'together':
|
case 'together':
|
||||||
case 'forwards':
|
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.
|
// This can end up rendering this component multiple passes.
|
||||||
// The first pass splits the children fibers into two sets. A head and tail.
|
// 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
|
// We first render the head. If anything is in fallback state, we do another
|
||||||
|
|
@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
|
||||||
validateTailOptions(tailMode, revealOrder);
|
validateTailOptions(tailMode, revealOrder);
|
||||||
validateSuspenseListChildren(newChildren, 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.
|
// Read how many children forks this set pushed so we can push it every time we retry.
|
||||||
const treeForkCount = getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
|
const treeForkCount = getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
|
||||||
|
|
||||||
|
|
@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
|
||||||
workInProgress.memoizedState = null;
|
workInProgress.memoizedState = null;
|
||||||
} else {
|
} else {
|
||||||
switch (revealOrder) {
|
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': {
|
case 'unstable_legacy-backwards': {
|
||||||
// We're going to find the first row that has existing content.
|
// 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
|
// At the same time we're going to reverse the list of everything
|
||||||
|
|
|
||||||
|
|
@ -1838,10 +1838,6 @@ function completeWork(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (renderState.isBackwards) {
|
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.
|
// Append to the beginning of the list.
|
||||||
renderedTail.sibling = workInProgress.child;
|
renderedTail.sibling = workInProgress.child;
|
||||||
workInProgress.child = renderedTail;
|
workInProgress.child = renderedTail;
|
||||||
|
|
|
||||||
|
|
@ -1022,7 +1022,7 @@ describe('ReactSuspenseList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate enableSuspenseList
|
// @gate enableSuspenseList
|
||||||
it('warns if revealOrder="backwards" is specified', async () => {
|
it('displays each items in "backwards" order', async () => {
|
||||||
const A = createAsyncText('A');
|
const A = createAsyncText('A');
|
||||||
const B = createAsyncText('B');
|
const B = createAsyncText('B');
|
||||||
const C = createAsyncText('C');
|
const C = createAsyncText('C');
|
||||||
|
|
@ -1030,14 +1030,14 @@ describe('ReactSuspenseList', () => {
|
||||||
function Foo() {
|
function Foo() {
|
||||||
return (
|
return (
|
||||||
<SuspenseList revealOrder="backwards" tail="visible">
|
<SuspenseList revealOrder="backwards" tail="visible">
|
||||||
<Suspense fallback={<Text text="Loading A" />}>
|
<Suspense fallback={<Text text="Loading C" />}>
|
||||||
<A />
|
<C />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={<Text text="Loading B" />}>
|
<Suspense fallback={<Text text="Loading B" />}>
|
||||||
<B />
|
<B />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={<Text text="Loading C" />}>
|
<Suspense fallback={<Text text="Loading A" />}>
|
||||||
<C />
|
<A />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</SuspenseList>
|
</SuspenseList>
|
||||||
);
|
);
|
||||||
|
|
@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
|
||||||
'Suspend! [C]',
|
'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(
|
expect(ReactNoop).toMatchRenderedOutput(
|
||||||
<>
|
<>
|
||||||
<span>Loading A</span>
|
<span>Loading A</span>
|
||||||
|
|
@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate enableSuspenseList
|
// @gate enableSuspenseList
|
||||||
it('displays each items in "backwards" order', async () => {
|
it('displays each items in "backwards" order (legacy)', async () => {
|
||||||
const A = createAsyncText('A');
|
const A = createAsyncText('A');
|
||||||
const B = createAsyncText('B');
|
const B = createAsyncText('B');
|
||||||
const C = createAsyncText('C');
|
const C = createAsyncText('C');
|
||||||
|
|
|
||||||
6
packages/react-server/src/ReactFizzServer.js
vendored
6
packages/react-server/src/ReactFizzServer.js
vendored
|
|
@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
|
||||||
const parentSegment = task.blockedSegment;
|
const parentSegment = task.blockedSegment;
|
||||||
const childIndex = parentSegment.children.length;
|
const childIndex = parentSegment.children.length;
|
||||||
const insertionIndex = parentSegment.chunks.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];
|
const node = rows[i];
|
||||||
task.row = previousSuspenseListRow = createSuspenseListRow(
|
task.row = previousSuspenseListRow = createSuspenseListRow(
|
||||||
previousSuspenseListRow,
|
previousSuspenseListRow,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user