mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Add tail="hidden" option to SuspenseList (#16024)
* Move misaligned comment * Add tail="hidden" option * isShowingAnyFallbacks -> findFirstSuspended * We can't reset Placement tags or we'll forget to insert them * Delete hasSuspendedChildrenAndNewContent optimization
This commit is contained in:
committed by
GitHub
parent
8d413bf2c3
commit
fcff9c57bc
+4
-3
@@ -27,7 +27,7 @@ import type {ReactEventComponentInstance} from 'shared/ReactTypes';
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import {enableProfilerTimer, enableFlareAPI} from 'shared/ReactFeatureFlags';
|
||||
import {NoEffect} from 'shared/ReactSideEffectTags';
|
||||
import {NoEffect, Placement} from 'shared/ReactSideEffectTags';
|
||||
import {ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
|
||||
import {
|
||||
IndeterminateComponent,
|
||||
@@ -494,8 +494,9 @@ export function resetWorkInProgress(
|
||||
// We assume pendingProps, index, key, ref, return are still untouched to
|
||||
// avoid doing another reconciliation.
|
||||
|
||||
// Reset the effect tag.
|
||||
workInProgress.effectTag = NoEffect;
|
||||
// Reset the effect tag but keep any Placement tags, since that's something
|
||||
// that child fiber is setting, not the reconciliation.
|
||||
workInProgress.effectTag &= Placement;
|
||||
|
||||
// The effect list is no longer valid.
|
||||
workInProgress.nextEffect = null;
|
||||
|
||||
+7
-7
@@ -125,7 +125,7 @@ import {
|
||||
addSubtreeSuspenseContext,
|
||||
setShallowSuspenseContext,
|
||||
} from './ReactFiberSuspenseContext';
|
||||
import {isShowingAnyFallbacks} from './ReactFiberSuspenseComponent';
|
||||
import {findFirstSuspended} from './ReactFiberSuspenseComponent';
|
||||
import {
|
||||
pushProvider,
|
||||
propagateContextChange,
|
||||
@@ -2000,7 +2000,7 @@ function findLastContentRow(firstChild: null | Fiber): null | Fiber {
|
||||
while (row !== null) {
|
||||
let currentRow = row.alternate;
|
||||
// New rows can't be content rows.
|
||||
if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
|
||||
if (currentRow !== null && findFirstSuspended(currentRow) === null) {
|
||||
lastContentRow = row;
|
||||
}
|
||||
row = row.sibling;
|
||||
@@ -2072,12 +2072,12 @@ function validateTailOptions(
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) {
|
||||
if (tailMode !== 'collapsed') {
|
||||
if (tailMode !== 'collapsed' && tailMode !== 'hidden') {
|
||||
didWarnAboutTailOptions[tailMode] = true;
|
||||
warning(
|
||||
false,
|
||||
'"%s" is not a supported value for tail on <SuspenseList />. ' +
|
||||
'Did you mean "collapsed"?',
|
||||
'Did you mean "collapsed" or "hidden"?',
|
||||
tailMode,
|
||||
);
|
||||
} else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') {
|
||||
@@ -2242,10 +2242,10 @@ function updateSuspenseListComponent(
|
||||
pushSuspenseContext(workInProgress, suspenseContext);
|
||||
|
||||
if ((workInProgress.mode & BatchedMode) === NoMode) {
|
||||
workInProgress.memoizedState = null;
|
||||
} else {
|
||||
// Outside of batched mode, SuspenseList doesn't work so we just
|
||||
// use make it a noop by treating it as the default revealOrder.
|
||||
workInProgress.memoizedState = null;
|
||||
} else {
|
||||
switch (revealOrder) {
|
||||
case 'forwards': {
|
||||
let lastContentRow = findLastContentRow(workInProgress.child);
|
||||
@@ -2281,7 +2281,7 @@ function updateSuspenseListComponent(
|
||||
while (row !== null) {
|
||||
let currentRow = row.alternate;
|
||||
// New rows can't be content rows.
|
||||
if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
|
||||
if (currentRow !== null && findFirstSuspended(currentRow) === null) {
|
||||
// This is the beginning of the main content.
|
||||
workInProgress.child = row;
|
||||
break;
|
||||
|
||||
+102
-112
@@ -92,7 +92,7 @@ import {
|
||||
ForceSuspenseFallback,
|
||||
setDefaultShallowSuspenseContext,
|
||||
} from './ReactFiberSuspenseContext';
|
||||
import {isShowingAnyFallbacks} from './ReactFiberSuspenseComponent';
|
||||
import {findFirstSuspended} from './ReactFiberSuspenseComponent';
|
||||
import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
popContext as popLegacyContext,
|
||||
@@ -537,6 +537,32 @@ function cutOffTailIfNeeded(
|
||||
hasRenderedATailFallback: boolean,
|
||||
) {
|
||||
switch (renderState.tailMode) {
|
||||
case 'hidden': {
|
||||
// Any insertions at the end of the tail list after this point
|
||||
// should be invisible. If there are already mounted boundaries
|
||||
// anything before them are not considered for collapsing.
|
||||
// Therefore we need to go through the whole tail to find if
|
||||
// there are any.
|
||||
let tailNode = renderState.tail;
|
||||
let lastTailNode = null;
|
||||
while (tailNode !== null) {
|
||||
if (tailNode.alternate !== null) {
|
||||
lastTailNode = tailNode;
|
||||
}
|
||||
tailNode = tailNode.sibling;
|
||||
}
|
||||
// Next we're simply going to delete all insertions after the
|
||||
// last rendered item.
|
||||
if (lastTailNode === null) {
|
||||
// All remaining items in the tail are insertions.
|
||||
renderState.tail = null;
|
||||
} else {
|
||||
// Detach the insertion after the last node that was already
|
||||
// inserted.
|
||||
lastTailNode.sibling = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'collapsed': {
|
||||
// Any insertions at the end of the tail list after this point
|
||||
// should be invisible. If there are already mounted boundaries
|
||||
@@ -572,89 +598,6 @@ function cutOffTailIfNeeded(
|
||||
}
|
||||
}
|
||||
|
||||
// Note this, might mutate the workInProgress passed in.
|
||||
function hasSuspendedChildrenAndNewContent(
|
||||
workInProgress: Fiber,
|
||||
firstChild: null | Fiber,
|
||||
): boolean {
|
||||
// Traversal to see if any of the immediately nested Suspense boundaries
|
||||
// are in their fallback states. I.e. something suspended in them.
|
||||
// And if some of them have new content that wasn't already visible.
|
||||
let hasSuspendedBoundaries = false;
|
||||
let hasNewContent = false;
|
||||
|
||||
let node = firstChild;
|
||||
while (node !== null) {
|
||||
// TODO: Hidden subtrees should not be considered.
|
||||
if (node.tag === SuspenseComponent) {
|
||||
const state: SuspenseState | null = node.memoizedState;
|
||||
const isShowingFallback = state !== null;
|
||||
if (isShowingFallback) {
|
||||
// Tag the parent fiber as having suspended boundaries.
|
||||
if (!hasSuspendedBoundaries) {
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
}
|
||||
|
||||
hasSuspendedBoundaries = true;
|
||||
|
||||
if (node.updateQueue !== null) {
|
||||
// If this is a newly suspended tree, it might not get committed as
|
||||
// part of the second pass. In that case nothing will subscribe to
|
||||
// its thennables. Instead, we'll transfer its thennables to the
|
||||
// SuspenseList so that it can retry if they resolve.
|
||||
// There might be multiple of these in the list but since we're
|
||||
// going to wait for all of them anyway, it doesn't really matter
|
||||
// which ones gets to ping. In theory we could get clever and keep
|
||||
// track of how many dependencies remain but it gets tricky because
|
||||
// in the meantime, we can add/remove/change items and dependencies.
|
||||
// We might bail out of the loop before finding any but that
|
||||
// doesn't matter since that means that the other boundaries that
|
||||
// we did find already has their listeners attached.
|
||||
workInProgress.updateQueue = node.updateQueue;
|
||||
workInProgress.effectTag |= Update;
|
||||
}
|
||||
} else {
|
||||
const current = node.alternate;
|
||||
const wasNotShowingContent =
|
||||
current === null || current.memoizedState !== null;
|
||||
if (wasNotShowingContent) {
|
||||
hasNewContent = true;
|
||||
}
|
||||
}
|
||||
if (hasSuspendedBoundaries && hasNewContent) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// TODO: We can probably just use the information from the list and not
|
||||
// drill into its children just like if it was a Suspense boundary.
|
||||
if (node.tag === SuspenseListComponent && node.updateQueue !== null) {
|
||||
// If there's a nested SuspenseList, we might have transferred
|
||||
// the thennables set to it already so we must get it from there.
|
||||
workInProgress.updateQueue = node.updateQueue;
|
||||
workInProgress.effectTag |= Update;
|
||||
}
|
||||
|
||||
if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (node === workInProgress) {
|
||||
return false;
|
||||
}
|
||||
while (node.sibling === null) {
|
||||
if (node.return === null || node.return === workInProgress) {
|
||||
return false;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function completeWork(
|
||||
current: Fiber | null,
|
||||
workInProgress: Fiber,
|
||||
@@ -988,7 +931,7 @@ function completeWork(
|
||||
if (!didSuspendAlready) {
|
||||
// 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.
|
||||
@@ -996,47 +939,93 @@ function completeWork(
|
||||
// We might be suspended if something in this render pass suspended, or
|
||||
// something in the previous committed pass suspended. Otherwise,
|
||||
// there's no chance so we can skip the expensive call to
|
||||
// hasSuspendedChildrenAndNewContent.
|
||||
// findFirstSuspended.
|
||||
let cannotBeSuspended =
|
||||
renderHasNotSuspendedYet() &&
|
||||
(current === null || (current.effectTag & DidCapture) === NoEffect);
|
||||
let needsRerender =
|
||||
!cannotBeSuspended &&
|
||||
hasSuspendedChildrenAndNewContent(workInProgress, renderedChildren);
|
||||
if (needsRerender) {
|
||||
// Rerender the whole list, but this time, we'll force fallbacks
|
||||
// to stay in place.
|
||||
// Reset the effect list before doing the second pass since that's now invalid.
|
||||
workInProgress.firstEffect = workInProgress.lastEffect = null;
|
||||
// Reset the child fibers to their original state.
|
||||
resetChildFibers(workInProgress, renderExpirationTime);
|
||||
if (!cannotBeSuspended) {
|
||||
let row = workInProgress.child;
|
||||
while (row !== null) {
|
||||
let suspended = findFirstSuspended(row);
|
||||
if (suspended !== null) {
|
||||
didSuspendAlready = true;
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
cutOffTailIfNeeded(renderState, false);
|
||||
|
||||
// Set up the Suspense Context to force suspense and immediately
|
||||
// rerender the children.
|
||||
pushSuspenseContext(
|
||||
workInProgress,
|
||||
setShallowSuspenseContext(
|
||||
suspenseStackCursor.current,
|
||||
ForceSuspenseFallback,
|
||||
),
|
||||
);
|
||||
return workInProgress.child;
|
||||
// If this is a newly suspended tree, it might not get committed as
|
||||
// part of the second pass. In that case nothing will subscribe to
|
||||
// its thennables. Instead, we'll transfer its thennables to the
|
||||
// SuspenseList so that it can retry if they resolve.
|
||||
// There might be multiple of these in the list but since we're
|
||||
// going to wait for all of them anyway, it doesn't really matter
|
||||
// which ones gets to ping. In theory we could get clever and keep
|
||||
// track of how many dependencies remain but it gets tricky because
|
||||
// in the meantime, we can add/remove/change items and dependencies.
|
||||
// We might bail out of the loop before finding any but that
|
||||
// doesn't matter since that means that the other boundaries that
|
||||
// we did find already has their listeners attached.
|
||||
let newThennables = suspended.updateQueue;
|
||||
if (newThennables !== null) {
|
||||
workInProgress.updateQueue = newThennables;
|
||||
workInProgress.effectTag |= Update;
|
||||
}
|
||||
|
||||
// Rerender the whole list, but this time, we'll force fallbacks
|
||||
// to stay in place.
|
||||
// Reset the effect list before doing the second pass since that's now invalid.
|
||||
workInProgress.firstEffect = workInProgress.lastEffect = null;
|
||||
// Reset the child fibers to their original state.
|
||||
resetChildFibers(workInProgress, renderExpirationTime);
|
||||
|
||||
// Set up the Suspense Context to force suspense and immediately
|
||||
// rerender the children.
|
||||
pushSuspenseContext(
|
||||
workInProgress,
|
||||
setShallowSuspenseContext(
|
||||
suspenseStackCursor.current,
|
||||
ForceSuspenseFallback,
|
||||
),
|
||||
);
|
||||
return workInProgress.child;
|
||||
}
|
||||
row = row.sibling;
|
||||
}
|
||||
}
|
||||
// hasSuspendedChildrenAndNewContent could've set didSuspendAlready
|
||||
didSuspendAlready =
|
||||
(workInProgress.effectTag & DidCapture) !== NoEffect;
|
||||
}
|
||||
if (didSuspendAlready) {
|
||||
} else {
|
||||
cutOffTailIfNeeded(renderState, false);
|
||||
}
|
||||
// Next we're going to render the tail.
|
||||
} else {
|
||||
// Append the rendered row to the child list.
|
||||
if (!didSuspendAlready) {
|
||||
if (isShowingAnyFallbacks(renderedTail)) {
|
||||
let suspended = findFirstSuspended(renderedTail);
|
||||
if (suspended !== null) {
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
didSuspendAlready = true;
|
||||
cutOffTailIfNeeded(renderState, true);
|
||||
// This might have been modified.
|
||||
if (
|
||||
renderState.tail === null &&
|
||||
renderState.tailMode === 'hidden'
|
||||
) {
|
||||
// We need to delete the row we just rendered.
|
||||
// Ensure we transfer the update queue to the parent.
|
||||
let newThennables = suspended.updateQueue;
|
||||
if (newThennables !== null) {
|
||||
workInProgress.updateQueue = newThennables;
|
||||
workInProgress.effectTag |= Update;
|
||||
}
|
||||
// Reset the effect list to what it w as before we rendered this
|
||||
// child. The nested children have already appended themselves.
|
||||
let lastEffect = (workInProgress.lastEffect =
|
||||
renderState.lastEffect);
|
||||
// Remove any effects that were appended after this point.
|
||||
if (lastEffect !== null) {
|
||||
lastEffect.nextEffect = null;
|
||||
}
|
||||
// We're done.
|
||||
return null;
|
||||
}
|
||||
} else if (
|
||||
now() > renderState.tailExpiration &&
|
||||
renderExpirationTime > Never
|
||||
@@ -1093,6 +1082,7 @@ function completeWork(
|
||||
let next = renderState.tail;
|
||||
renderState.rendering = next;
|
||||
renderState.tail = next.sibling;
|
||||
renderState.lastEffect = workInProgress.lastEffect;
|
||||
next.sibling = null;
|
||||
|
||||
// Restore the context.
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
*/
|
||||
|
||||
import type {Fiber} from './ReactFiber';
|
||||
import {SuspenseComponent} from 'shared/ReactWorkTags';
|
||||
import {SuspenseComponent, SuspenseListComponent} from 'shared/ReactWorkTags';
|
||||
import {NoEffect, DidCapture} from 'shared/ReactSideEffectTags';
|
||||
|
||||
// TODO: This is now an empty object. Should we switch this to a boolean?
|
||||
// Alternatively we can make this use an effect tag similar to SuspenseList.
|
||||
export type SuspenseState = {||};
|
||||
|
||||
export type SuspenseListTailMode = 'collapsed' | void;
|
||||
export type SuspenseListTailMode = 'collapsed' | 'hidden' | void;
|
||||
|
||||
export type SuspenseListRenderState = {|
|
||||
isBackwards: boolean,
|
||||
@@ -28,6 +29,9 @@ export type SuspenseListRenderState = {|
|
||||
tailExpiration: number,
|
||||
// Tail insertions setting.
|
||||
tailMode: SuspenseListTailMode,
|
||||
// Last Effect before we rendered the "rendering" item.
|
||||
// Used to remove new effects added by the rendered item.
|
||||
lastEffect: null | Fiber,
|
||||
|};
|
||||
|
||||
export function shouldCaptureSuspense(
|
||||
@@ -58,13 +62,23 @@ export function shouldCaptureSuspense(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isShowingAnyFallbacks(row: Fiber): boolean {
|
||||
export function findFirstSuspended(row: Fiber): null | Fiber {
|
||||
let node = row;
|
||||
while (node !== null) {
|
||||
if (node.tag === SuspenseComponent) {
|
||||
const state: SuspenseState | null = node.memoizedState;
|
||||
if (state !== null) {
|
||||
return true;
|
||||
return node;
|
||||
}
|
||||
} else if (
|
||||
node.tag === SuspenseListComponent &&
|
||||
// revealOrder undefined can't be trusted because it don't
|
||||
// keep track of whether it suspended or not.
|
||||
node.memoizedProps.revealOrder !== undefined
|
||||
) {
|
||||
let didSuspend = (node.effectTag & DidCapture) !== NoEffect;
|
||||
if (didSuspend) {
|
||||
return node;
|
||||
}
|
||||
} else if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
@@ -72,16 +86,16 @@ export function isShowingAnyFallbacks(row: Fiber): boolean {
|
||||
continue;
|
||||
}
|
||||
if (node === row) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
while (node.sibling === null) {
|
||||
if (node.return === null || node.return === row) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -640,6 +640,9 @@ describe('ReactSuspenseList', () => {
|
||||
'Loading B',
|
||||
'Suspend! [C]',
|
||||
'Loading C',
|
||||
'A',
|
||||
'Loading B',
|
||||
'Loading C',
|
||||
]);
|
||||
|
||||
// This will suspend, since the boundaries are avoided. Give them
|
||||
@@ -859,6 +862,10 @@ describe('ReactSuspenseList', () => {
|
||||
'Suspend! [C]',
|
||||
'Loading C',
|
||||
'D',
|
||||
'Loading A',
|
||||
'B',
|
||||
'Loading C',
|
||||
'D',
|
||||
'Loading E',
|
||||
'Loading F',
|
||||
]);
|
||||
@@ -1013,6 +1020,16 @@ describe('ReactSuspenseList', () => {
|
||||
'E',
|
||||
'Suspend! [F]',
|
||||
'Loading F',
|
||||
'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
|
||||
@@ -1251,7 +1268,7 @@ describe('ReactSuspenseList', () => {
|
||||
|
||||
expect(() => Scheduler.unstable_flushAll()).toWarnDev([
|
||||
'Warning: "collapse" is not a supported value for tail on ' +
|
||||
'<SuspenseList />. Did you mean "collapsed"?' +
|
||||
'<SuspenseList />. Did you mean "collapsed" or "hidden"?' +
|
||||
'\n in SuspenseList (at **)' +
|
||||
'\n in Foo (at **)',
|
||||
]);
|
||||
@@ -1392,6 +1409,10 @@ describe('ReactSuspenseList', () => {
|
||||
'Suspend! [C]',
|
||||
'Loading C',
|
||||
'D',
|
||||
'A',
|
||||
'Loading B',
|
||||
'Loading C',
|
||||
'D',
|
||||
'Loading E',
|
||||
]);
|
||||
|
||||
@@ -1516,6 +1537,10 @@ describe('ReactSuspenseList', () => {
|
||||
'Suspend! [E]',
|
||||
'Loading E',
|
||||
'F',
|
||||
'C',
|
||||
'Loading D',
|
||||
'Loading E',
|
||||
'F',
|
||||
'Loading B',
|
||||
]);
|
||||
|
||||
@@ -1530,15 +1555,15 @@ describe('ReactSuspenseList', () => {
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await E.resolve();
|
||||
await D.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [D]', 'E']);
|
||||
expect(Scheduler).toFlushAndYield(['D', 'Suspend! [E]']);
|
||||
|
||||
// Incremental loading is suspended.
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
// Even though E is unsuspended, it's still in loading state because
|
||||
// it is blocked by D.
|
||||
// Even though D is unsuspended, it's still in loading state because
|
||||
// it is blocked by E.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>Loading B</span>
|
||||
@@ -1650,6 +1675,11 @@ describe('ReactSuspenseList', () => {
|
||||
'Loading C',
|
||||
'Suspend! [D]',
|
||||
'Loading D',
|
||||
'A',
|
||||
'Loading B',
|
||||
'Loading C',
|
||||
'Suspend! [D]',
|
||||
'Loading D',
|
||||
'Loading E',
|
||||
]);
|
||||
|
||||
@@ -1733,4 +1763,67 @@ describe('ReactSuspenseList', () => {
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('only shows no initial loading state "hidden" tail insertions', async () => {
|
||||
let A = createAsyncText('A');
|
||||
let B = createAsyncText('B');
|
||||
let C = createAsyncText('C');
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="forwards" tail="hidden">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
|
||||
await A.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'Loading B']);
|
||||
|
||||
// Incremental loading is suspended.
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
|
||||
|
||||
await B.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['B', 'Suspend! [C]', 'Loading C']);
|
||||
|
||||
// Incremental loading is suspended.
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
</Fragment>,
|
||||
);
|
||||
|
||||
await C.resolve();
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['C']);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<Fragment>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user