Skip to content
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
102 changes: 89 additions & 13 deletions packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -157,31 +157,38 @@ describe('ReactDOMFizzSuspenseList', () => {
);
}

await A.resolve();
await C.resolve();

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
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(
<div>
<span>A</span>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);

await serverAct(() => C.resolve());
assertLog(['C']);
await serverAct(() => A.resolve());
assertLog(['A']);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>Loading B</span>
<span>C</span>
<span>Loading C</span>
</div>,
);

Expand Down Expand Up @@ -649,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
it('displays each items in "backwards" order in legacy mode', async () => {
const A = createAsyncText('A');
Expand Down Expand Up @@ -730,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
return (
<div>
<SuspenseList revealOrder="forwards" tail="visible">
<SuspenseList
revealOrder="unstable_legacy-backwards"
tail="visible">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<SuspenseList revealOrder="backwards" tail="visible">
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
</SuspenseList>
<Suspense fallback={<Text text="Loading C" />}>
<C />
Expand Down
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -2104,7 +2104,8 @@ export function validateSuspenseListChildren(
) {
if (__DEV__) {
if (
(revealOrder === 'forwards' ||
(revealOrder == null ||
revealOrder === 'forwards' ||
revealOrder === 'backwards' ||
revealOrder === 'unstable_legacy-backwards') &&
children !== undefined &&
Expand Down
102 changes: 68 additions & 34 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -3245,25 +3245,16 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
if (__DEV__) {
const cacheKey = revealOrder == null ? 'null' : revealOrder;
if (
revealOrder != null &&
revealOrder !== 'forwards' &&
revealOrder !== 'backwards' &&
revealOrder !== 'unstable_legacy-backwards' &&
revealOrder !== 'together' &&
revealOrder !== 'independent' &&
!didWarnAboutRevealOrder[cacheKey]
) {
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') {
if (typeof revealOrder === 'string') {
switch (revealOrder.toLowerCase()) {
case 'together':
case 'forwards':
Expand Down Expand Up @@ -3314,18 +3305,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 <SuspenseList tail="..."> 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' &&
Expand All @@ -3338,14 +3318,15 @@ function validateTailOptions(
tailMode,
);
} else if (
revealOrder != null &&
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". ' +
'"forwards" (default) or "backwards". ' +
'Did you mean to specify revealOrder="forwards"?',
tailMode,
);
Expand Down Expand Up @@ -3386,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
Expand Down Expand Up @@ -3424,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;

Expand All @@ -3449,31 +3450,37 @@ function updateSuspenseListComponent(
workInProgress.memoizedState = null;
} else {
switch (revealOrder) {
case 'forwards': {
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.
// 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.
// 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,
false, // isBackwards
true, // isBackwards
tail,
lastContentRow,
null, // last
tailMode,
treeForkCount,
);
break;
}
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
Expand Down Expand Up @@ -3517,10 +3524,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;
}
}
}
Expand Down
Loading
Loading