mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Add forwards and backwards options to SuspenseList (#15918)
* Add forwards option * Add backwards option * Add comment * Add customized warning messages for case and typos * Add some more tests for insertions and updates in start/middle/end
This commit is contained in:
+149
-8
@@ -127,6 +127,7 @@ import {
|
||||
addSubtreeSuspenseContext,
|
||||
setShallowSuspenseContext,
|
||||
} from './ReactFiberSuspenseContext';
|
||||
import {isShowingAnyFallbacks} from './ReactFiberSuspenseComponent';
|
||||
import {
|
||||
pushProvider,
|
||||
propagateContextChange,
|
||||
@@ -1983,8 +1984,36 @@ function propagateSuspenseContextChange(
|
||||
}
|
||||
}
|
||||
|
||||
type SuspenseListRevealOrder = 'together' | void;
|
||||
function findLastContentRow(firstChild: null | Fiber): null | Fiber {
|
||||
// This is going to find the last row among these children that is already
|
||||
// showing content on the screen, as opposed to being in fallback state or
|
||||
// new. If a row has multiple Suspense boundaries, any of them being in the
|
||||
// fallback state, counts as the whole row being in a fallback state.
|
||||
// Note that the "rows" will be workInProgress, but any nested children
|
||||
// will still be current since we haven't rendered them yet. The mounted
|
||||
// order may not be the same as the new order. We use the new order.
|
||||
let row = firstChild;
|
||||
let lastContentRow: null | Fiber = null;
|
||||
while (row !== null) {
|
||||
let currentRow = row.alternate;
|
||||
// New rows can't be content rows.
|
||||
if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
|
||||
lastContentRow = row;
|
||||
}
|
||||
row = row.sibling;
|
||||
}
|
||||
return lastContentRow;
|
||||
}
|
||||
|
||||
type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void;
|
||||
|
||||
// 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
|
||||
// pass through beginWork to rerender all children (including the tail) with
|
||||
// the force suspend context. If the first render didn't have anything in
|
||||
// in fallback state. Then we render each row in the tail one-by-one.
|
||||
// That happens in the completeWork phase without going back to beginWork.
|
||||
function updateSuspenseListComponent(
|
||||
current: Fiber | null,
|
||||
workInProgress: Fiber,
|
||||
@@ -2033,6 +2062,10 @@ function updateSuspenseListComponent(
|
||||
);
|
||||
suspenseListState = {
|
||||
didSuspend: true,
|
||||
isBackwards: false,
|
||||
rendering: null,
|
||||
last: null,
|
||||
tail: null,
|
||||
};
|
||||
} else {
|
||||
let didForceFallback =
|
||||
@@ -2063,7 +2096,79 @@ function updateSuspenseListComponent(
|
||||
}
|
||||
|
||||
switch (revealOrder) {
|
||||
// TODO: For other reveal orders we'll need to split the nextChildFibers set.
|
||||
case 'forwards': {
|
||||
// If need to force fallbacks in this pass we're just going to
|
||||
// force the whole set to suspend so we don't have to do anything
|
||||
// further here.
|
||||
if (!shouldForceFallback) {
|
||||
let lastContentRow = findLastContentRow(nextChildFibers);
|
||||
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 = nextChildFibers;
|
||||
nextChildFibers = null;
|
||||
} else {
|
||||
// Disconnect the tail rows after the content row.
|
||||
// We're going to render them separately later.
|
||||
tail = lastContentRow.sibling;
|
||||
lastContentRow.sibling = null;
|
||||
}
|
||||
if (suspenseListState === null) {
|
||||
suspenseListState = {
|
||||
didSuspend: false,
|
||||
isBackwards: false,
|
||||
rendering: null,
|
||||
last: lastContentRow,
|
||||
tail: tail,
|
||||
};
|
||||
} else {
|
||||
suspenseListState.tail = tail;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'backwards': {
|
||||
// If need to force fallbacks in this pass we're just going to
|
||||
// force the whole set to suspend so we don't have to do anything
|
||||
// further here.
|
||||
if (!shouldForceFallback) {
|
||||
// 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
|
||||
// we pass in the meantime. That's going to be our tail in reverse
|
||||
// order.
|
||||
let tail = null;
|
||||
let row = nextChildFibers;
|
||||
nextChildFibers = null;
|
||||
while (row !== null) {
|
||||
let currentRow = row.alternate;
|
||||
// New rows can't be content rows.
|
||||
if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
|
||||
// This is the beginning of the main content.
|
||||
nextChildFibers = row;
|
||||
break;
|
||||
}
|
||||
let nextRow = row.sibling;
|
||||
row.sibling = tail;
|
||||
tail = row;
|
||||
row = nextRow;
|
||||
}
|
||||
// TODO: If nextChildFibers is null, we can continue on the tail immediately.
|
||||
if (suspenseListState === null) {
|
||||
suspenseListState = {
|
||||
didSuspend: false,
|
||||
isBackwards: true,
|
||||
rendering: null,
|
||||
last: null,
|
||||
tail: tail,
|
||||
};
|
||||
} else {
|
||||
suspenseListState.isBackwards = true;
|
||||
suspenseListState.tail = tail;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'together': {
|
||||
break;
|
||||
}
|
||||
@@ -2076,12 +2181,48 @@ function updateSuspenseListComponent(
|
||||
!didWarnAboutRevealOrder[revealOrder]
|
||||
) {
|
||||
didWarnAboutRevealOrder[revealOrder] = true;
|
||||
warning(
|
||||
false,
|
||||
'"%s" is not a supported revealOrder on <SuspenseList />. ' +
|
||||
'Did you mean "together"?',
|
||||
revealOrder,
|
||||
);
|
||||
if (typeof revealOrder === 'string') {
|
||||
switch (revealOrder.toLowerCase()) {
|
||||
case 'together':
|
||||
case 'forwards':
|
||||
case 'backwards': {
|
||||
warning(
|
||||
false,
|
||||
'"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
|
||||
'Use lowercase "%s" instead.',
|
||||
revealOrder,
|
||||
revealOrder.toLowerCase(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'forward':
|
||||
case 'backward': {
|
||||
warning(
|
||||
false,
|
||||
'"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
|
||||
'React uses the -s suffix in the spelling. Use "%ss" instead.',
|
||||
revealOrder,
|
||||
revealOrder.toLowerCase(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
warning(
|
||||
false,
|
||||
'"%s" is not a supported revealOrder on <SuspenseList />. ' +
|
||||
'Did you mean "together", "forwards" or "backwards"?',
|
||||
revealOrder,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
warning(
|
||||
false,
|
||||
'%s is not a supported value for revealOrder on <SuspenseList />. ' +
|
||||
'Did you mean "together", "forwards" or "backwards"?',
|
||||
revealOrder,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// We mark this as having captured but it really just says to the
|
||||
|
||||
+83
-20
@@ -18,7 +18,10 @@ import type {
|
||||
ChildSet,
|
||||
} from './ReactFiberHostConfig';
|
||||
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent';
|
||||
import type {
|
||||
SuspenseState,
|
||||
SuspenseListState,
|
||||
} from './ReactFiberSuspenseComponent';
|
||||
import type {SuspenseContext} from './ReactFiberSuspenseContext';
|
||||
|
||||
import {
|
||||
@@ -84,7 +87,12 @@ import {
|
||||
InvisibleParentSuspenseContext,
|
||||
hasSuspenseContext,
|
||||
popSuspenseContext,
|
||||
pushSuspenseContext,
|
||||
setShallowSuspenseContext,
|
||||
ForceSuspenseFallback,
|
||||
setDefaultShallowSuspenseContext,
|
||||
} from './ReactFiberSuspenseContext';
|
||||
import {isShowingAnyFallbacks} from './ReactFiberSuspenseComponent';
|
||||
import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
popContext as popLegacyContext,
|
||||
@@ -917,25 +925,80 @@ function completeWork(
|
||||
popSuspenseContext(workInProgress);
|
||||
|
||||
if ((workInProgress.effectTag & DidCapture) === NoEffect) {
|
||||
// This is the first pass. We need to figure out if anything is still
|
||||
// suspended in the rendered set.
|
||||
const renderedChildren = workInProgress.child;
|
||||
// If new content unsuspended, but there's still some content that
|
||||
// didn't. Then we need to do a second pass that forces everything
|
||||
// to keep showing their fallbacks.
|
||||
const needsRerender = hasSuspendedChildrenAndNewContent(
|
||||
workInProgress,
|
||||
renderedChildren,
|
||||
);
|
||||
if (needsRerender) {
|
||||
// Rerender the whole list, but this time, we'll force fallbacks
|
||||
// to stay in place.
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
// Reset the effect list before doing the second pass since that's now invalid.
|
||||
workInProgress.firstEffect = workInProgress.lastEffect = null;
|
||||
// Schedule work so we know not to bail out.
|
||||
workInProgress.expirationTime = renderExpirationTime;
|
||||
return workInProgress;
|
||||
let suspenseListState: null | SuspenseListState =
|
||||
workInProgress.memoizedState;
|
||||
if (
|
||||
suspenseListState === null ||
|
||||
suspenseListState.rendering === null
|
||||
) {
|
||||
// This is the first pass. We need to figure out if anything is still
|
||||
// suspended in the rendered set.
|
||||
const renderedChildren = workInProgress.child;
|
||||
// If new content unsuspended, but there's still some content that
|
||||
// didn't. Then we need to do a second pass that forces everything
|
||||
// to keep showing their fallbacks.
|
||||
const needsRerender = hasSuspendedChildrenAndNewContent(
|
||||
workInProgress,
|
||||
renderedChildren,
|
||||
);
|
||||
if (needsRerender) {
|
||||
// Rerender the whole list, but this time, we'll force fallbacks
|
||||
// to stay in place.
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
// Reset the effect list before doing the second pass since that's now invalid.
|
||||
workInProgress.firstEffect = workInProgress.lastEffect = null;
|
||||
// Schedule work so we know not to bail out.
|
||||
workInProgress.expirationTime = renderExpirationTime;
|
||||
return workInProgress;
|
||||
}
|
||||
} else {
|
||||
// Append the rendered row to the child list.
|
||||
let rendered = suspenseListState.rendering;
|
||||
if (!suspenseListState.didSuspend) {
|
||||
suspenseListState.didSuspend = isShowingAnyFallbacks(rendered);
|
||||
}
|
||||
if (suspenseListState.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.
|
||||
rendered.sibling = workInProgress.child;
|
||||
workInProgress.child = rendered;
|
||||
} else {
|
||||
let previousSibling = suspenseListState.last;
|
||||
if (previousSibling !== null) {
|
||||
previousSibling.sibling = rendered;
|
||||
} else {
|
||||
workInProgress.child = rendered;
|
||||
}
|
||||
suspenseListState.last = rendered;
|
||||
}
|
||||
}
|
||||
|
||||
if (suspenseListState !== null && suspenseListState.tail !== null) {
|
||||
// We still have tail rows to render.
|
||||
// Pop a row.
|
||||
let next = suspenseListState.tail;
|
||||
suspenseListState.rendering = next;
|
||||
suspenseListState.tail = next.sibling;
|
||||
next.sibling = null;
|
||||
|
||||
// Restore the context.
|
||||
// TODO: We can probably just avoid popping it instead and only
|
||||
// setting it the first time we go from not suspended to suspended.
|
||||
let suspenseContext = suspenseStackCursor.current;
|
||||
if (suspenseListState.didSuspend) {
|
||||
suspenseContext = setShallowSuspenseContext(
|
||||
suspenseContext,
|
||||
ForceSuspenseFallback,
|
||||
);
|
||||
} else {
|
||||
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
|
||||
}
|
||||
pushSuspenseContext(workInProgress, suspenseContext);
|
||||
// Do a pass over the next row.
|
||||
return next;
|
||||
}
|
||||
} else {
|
||||
workInProgress.effectTag &= ~DidCapture;
|
||||
|
||||
@@ -8,12 +8,20 @@
|
||||
*/
|
||||
|
||||
import type {Fiber} from './ReactFiber';
|
||||
import {SuspenseComponent} from 'shared/ReactWorkTags';
|
||||
|
||||
// TODO: This is now an empty object. Should we switch this to a boolean?
|
||||
export type SuspenseState = {||};
|
||||
|
||||
export type SuspenseListState = {|
|
||||
didSuspend: boolean,
|
||||
isBackwards: boolean,
|
||||
// The currently rendering tail row.
|
||||
rendering: null | Fiber,
|
||||
// The last of the already rendered children.
|
||||
last: null | Fiber,
|
||||
// Remaining rows on the tail of the list.
|
||||
tail: null | Fiber,
|
||||
|};
|
||||
|
||||
export function shouldCaptureSuspense(
|
||||
@@ -43,3 +51,31 @@ export function shouldCaptureSuspense(
|
||||
// If the parent is not able to handle it, we must handle it.
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isShowingAnyFallbacks(row: Fiber): boolean {
|
||||
let node = row;
|
||||
while (node !== null) {
|
||||
if (node.tag === SuspenseComponent) {
|
||||
const state: SuspenseState | null = node.memoizedState;
|
||||
if (state !== null) {
|
||||
return true;
|
||||
}
|
||||
} else if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
if (node === row) {
|
||||
return false;
|
||||
}
|
||||
while (node.sibling === null) {
|
||||
if (node.return === null || node.return === row) {
|
||||
return false;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,46 @@ describe('ReactSuspenseList', () => {
|
||||
|
||||
expect(() => Scheduler.flushAll()).toWarnDev([
|
||||
'Warning: "something" is not a supported revealOrder on ' +
|
||||
'<SuspenseList />. Did you mean "together"?' +
|
||||
'<SuspenseList />. Did you mean "together", "forwards" or "backwards"?' +
|
||||
'\n in SuspenseList (at **)' +
|
||||
'\n in Foo (at **)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('warns if a upper case revealOrder option is used', () => {
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="TOGETHER">
|
||||
<Suspense fallback="Loading">Content</Suspense>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
|
||||
expect(() => Scheduler.flushAll()).toWarnDev([
|
||||
'Warning: "TOGETHER" is not a valid value for revealOrder on ' +
|
||||
'<SuspenseList />. Use lowercase "together" instead.' +
|
||||
'\n in SuspenseList (at **)' +
|
||||
'\n in Foo (at **)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('warns if a misspelled revealOrder option is used', () => {
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="forward">
|
||||
<Suspense fallback="Loading">Content</Suspense>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
|
||||
expect(() => Scheduler.flushAll()).toWarnDev([
|
||||
'Warning: "forward" is not a valid value for revealOrder on ' +
|
||||
'<SuspenseList />. React uses the -s suffix in the spelling. ' +
|
||||
'Use "forwards" instead.' +
|
||||
'\n in SuspenseList (at **)' +
|
||||
'\n in Foo (at **)',
|
||||
]);
|
||||
@@ -562,4 +601,430 @@ describe('ReactSuspenseList', () => {
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays each items in "forwards" order', async () => {
|
||||
let A = createAsyncText('A');
|
||||
let B = createAsyncText('B');
|
||||
let C = createAsyncText('C');
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<Suspense fallback={<Text text="Loading A" />}>
|
||||
<A />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Text text="Loading B" />}>
|
||||
<B />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Text text="Loading C" />}>
|
||||
<C />
|
||||
</Suspense>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await C.resolve();
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Suspend! [A]',
|
||||
'Loading A',
|
||||
'Loading B',
|
||||
'Loading C',
|
||||
]);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>Loading A</span>
|
||||
<span>Loading B</span>
|
||||
<span>Loading C</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await A.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>Loading B</span>
|
||||
<span>Loading C</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await B.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['B', 'C']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays each items in "backwards" order', async () => {
|
||||
let A = createAsyncText('A');
|
||||
let B = createAsyncText('B');
|
||||
let C = createAsyncText('C');
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="backwards">
|
||||
<Suspense fallback={<Text text="Loading A" />}>
|
||||
<A />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Text text="Loading B" />}>
|
||||
<B />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Text text="Loading C" />}>
|
||||
<C />
|
||||
</Suspense>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await A.resolve();
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Suspend! [C]',
|
||||
'Loading C',
|
||||
'Loading B',
|
||||
'Loading A',
|
||||
]);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>Loading A</span>
|
||||
<span>Loading B</span>
|
||||
<span>Loading C</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await C.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['C', 'Suspend! [B]']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>Loading A</span>
|
||||
<span>Loading B</span>
|
||||
<span>C</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await B.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['B', 'A']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
|
||||
let A = createAsyncText('A');
|
||||
let B = createAsyncText('B');
|
||||
let C = createAsyncText('C');
|
||||
let D = createAsyncText('D');
|
||||
let E = createAsyncText('E');
|
||||
let F = createAsyncText('F');
|
||||
|
||||
function Foo({items}) {
|
||||
return (
|
||||
<SuspenseList revealOrder="forwards">
|
||||
{items.map(([key, Component]) => (
|
||||
<Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
))}
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await B.resolve();
|
||||
await D.resolve();
|
||||
|
||||
ReactNoop.render(<Foo items={[['B', B], ['D', D]]} />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['B', 'D']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>B</span>
|
||||
<span>D</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
// Insert items in the beginning, middle and end.
|
||||
ReactNoop.render(
|
||||
<Foo
|
||||
items={[['A', A], ['B', B], ['C', C], ['D', D], ['E', E], ['F', F]]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Suspend! [A]',
|
||||
'Loading A',
|
||||
'B',
|
||||
'Suspend! [C]',
|
||||
'Loading C',
|
||||
'D',
|
||||
'Suspend! [E]',
|
||||
'Loading E',
|
||||
'Loading F',
|
||||
]);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>Loading A</span>
|
||||
<span>B</span>
|
||||
<span>Loading C</span>
|
||||
<span>D</span>
|
||||
<span>Loading E</span>
|
||||
<span>Loading F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await A.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [C]']);
|
||||
|
||||
// Even though we could show A, it is still in a fallback state because
|
||||
// C is not yet resolved. We need to resolve everything in the head first.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>Loading A</span>
|
||||
<span>B</span>
|
||||
<span>Loading C</span>
|
||||
<span>D</span>
|
||||
<span>Loading E</span>
|
||||
<span>Loading F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await C.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['A', 'C', 'Suspend! [E]']);
|
||||
|
||||
// We can now resolve the full head.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>Loading E</span>
|
||||
<span>Loading F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await E.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['E', 'Suspend! [F]']);
|
||||
|
||||
// In the tail we can resolve one-by-one.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>Loading F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await F.resolve();
|
||||
|
||||
// We can also delete some items.
|
||||
ReactNoop.render(<Foo items={[['D', D], ['E', E], ['F', F]]} />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['D', 'E', 'F']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
|
||||
let A = createAsyncText('A');
|
||||
let B = createAsyncText('B');
|
||||
let D = createAsyncText('D');
|
||||
let F = createAsyncText('F');
|
||||
|
||||
function createSyncText(text) {
|
||||
return function() {
|
||||
return <Text text={text} />;
|
||||
};
|
||||
}
|
||||
|
||||
let As = createSyncText('A');
|
||||
let Bs = createSyncText('B');
|
||||
let Cs = createSyncText('C');
|
||||
let Ds = createSyncText('D');
|
||||
let Es = createSyncText('E');
|
||||
let Fs = createSyncText('F');
|
||||
|
||||
function Foo({items}) {
|
||||
return (
|
||||
<SuspenseList revealOrder="backwards">
|
||||
{items.map(([key, Component]) => (
|
||||
<Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
))}
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
// The first pass doesn't suspend.
|
||||
ReactNoop.render(
|
||||
<Foo
|
||||
items={[
|
||||
['A', As],
|
||||
['B', Bs],
|
||||
['C', Cs],
|
||||
['D', Ds],
|
||||
['E', Es],
|
||||
['F', Fs],
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield(['F', 'E', 'D', 'C', 'B', 'A']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
// Update items in the beginning, middle and end to start suspending.
|
||||
ReactNoop.render(
|
||||
<Foo
|
||||
items={[['A', A], ['B', B], ['C', Cs], ['D', D], ['E', Es], ['F', F]]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Suspend! [A]',
|
||||
'Loading A',
|
||||
'Suspend! [B]',
|
||||
'Loading B',
|
||||
'C',
|
||||
'Suspend! [D]',
|
||||
'Loading D',
|
||||
'E',
|
||||
'Suspend! [F]',
|
||||
'Loading F',
|
||||
]);
|
||||
|
||||
// This will suspend, since the boundaries are avoided. Give them
|
||||
// time to display their loading states.
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>Loading A</span>
|
||||
<span hidden={true}>B</span>
|
||||
<span>Loading B</span>
|
||||
<span>C</span>
|
||||
<span hidden={true}>D</span>
|
||||
<span>Loading D</span>
|
||||
<span>E</span>
|
||||
<span hidden={true}>F</span>
|
||||
<span>Loading F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await F.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['F']);
|
||||
|
||||
// Even though we could show F, it is still in a fallback state because
|
||||
// E is not yet resolved. We need to resolve everything in the head first.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>Loading A</span>
|
||||
<span hidden={true}>B</span>
|
||||
<span>Loading B</span>
|
||||
<span>C</span>
|
||||
<span hidden={true}>D</span>
|
||||
<span>Loading D</span>
|
||||
<span>E</span>
|
||||
<span hidden={true}>F</span>
|
||||
<span>Loading F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await D.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['D', 'F', 'Suspend! [B]']);
|
||||
|
||||
// We can now resolve the full head.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>Loading A</span>
|
||||
<span hidden={true}>B</span>
|
||||
<span>Loading B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await B.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['B', 'Suspend! [A]']);
|
||||
|
||||
// In the tail we can resolve one-by-one.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>Loading A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await A.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['A']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user