Skip to content

Commit

Permalink
Fix suspense fallback throttling (#24253)
Browse files Browse the repository at this point in the history
* fix suspense throttling

* fix lint

* Tweak tests + another test

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
  • Loading branch information
2 people authored and rickhanlonii committed Apr 19, 2022
1 parent 9f2e44e commit c6cca64
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 6 deletions.
11 changes: 8 additions & 3 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Expand Up @@ -2221,17 +2221,22 @@ function commitMutationEffectsOnFiber(
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);

if (flags & Visibility) {
const newState: OffscreenState | null = finishedWork.memoizedState;
const offscreenFiber: Fiber = (finishedWork.child: any);

if (offscreenFiber.flags & Visibility) {
const newState: OffscreenState | null = offscreenFiber.memoizedState;
const isHidden = newState !== null;
if (isHidden) {
const wasHidden = current !== null && current.memoizedState !== null;
const wasHidden =
offscreenFiber.alternate !== null &&
offscreenFiber.alternate.memoizedState !== null;
if (!wasHidden) {
// TODO: Move to passive phase
markCommitTimeOfFallback();
}
}
}

if (flags & Update) {
try {
commitSuspenseCallback(finishedWork);
Expand Down
11 changes: 8 additions & 3 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Expand Up @@ -2221,17 +2221,22 @@ function commitMutationEffectsOnFiber(
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);

if (flags & Visibility) {
const newState: OffscreenState | null = finishedWork.memoizedState;
const offscreenFiber: Fiber = (finishedWork.child: any);

if (offscreenFiber.flags & Visibility) {
const newState: OffscreenState | null = offscreenFiber.memoizedState;
const isHidden = newState !== null;
if (isHidden) {
const wasHidden = current !== null && current.memoizedState !== null;
const wasHidden =
offscreenFiber.alternate !== null &&
offscreenFiber.alternate.memoizedState !== null;
if (!wasHidden) {
// TODO: Move to passive phase
markCommitTimeOfFallback();
}
}
}

if (flags & Update) {
try {
commitSuspenseCallback(finishedWork);
Expand Down
111 changes: 111 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
Expand Up @@ -285,6 +285,117 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('AsyncAfter SuspenseSibling');
});

it('throttles fallback committing globally', () => {
function Foo() {
Scheduler.unstable_yieldValue('Foo');
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" ms={200} />
<Suspense fallback={<Text text="Loading more..." />}>
<AsyncText text="B" ms={300} />
</Suspense>
</Suspense>
);
}

// Committing fallbacks should be throttled.
// First, advance some time to skip the first threshold.
jest.advanceTimersByTime(600);
Scheduler.unstable_advanceTime(600);

const root = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});

expect(Scheduler).toFlushAndYield([
'Foo',
'Suspend! [A]',
'Suspend! [B]',
'Loading more...',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...');

// Resolve A.
jest.advanceTimersByTime(200);
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'Loading more...']);

// By this point, we have enough info to show "A" and "Loading more..."
// However, we've just shown the outer fallback. So we'll delay
// showing the inner fallback hoping that B will resolve soon enough.
expect(root).toMatchRenderedOutput('Loading...');

// Resolve B.
jest.advanceTimersByTime(100);
Scheduler.unstable_advanceTime(100);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);

// By this point, B has resolved.
// We're still showing the outer fallback.
expect(root).toMatchRenderedOutput('Loading...');
expect(Scheduler).toFlushAndYield(['A', 'B']);
// Then contents of both should pop in together.
expect(root).toMatchRenderedOutput('AB');
});

it('does not throttle fallback committing for too long', () => {
function Foo() {
Scheduler.unstable_yieldValue('Foo');
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" ms={200} />
<Suspense fallback={<Text text="Loading more..." />}>
<AsyncText text="B" ms={1200} />
</Suspense>
</Suspense>
);
}

// Committing fallbacks should be throttled.
// First, advance some time to skip the first threshold.
jest.advanceTimersByTime(600);
Scheduler.unstable_advanceTime(600);

const root = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});

expect(Scheduler).toFlushAndYield([
'Foo',
'Suspend! [A]',
'Suspend! [B]',
'Loading more...',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...');

// Resolve A.
jest.advanceTimersByTime(200);
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'Loading more...']);

// By this point, we have enough info to show "A" and "Loading more..."
// However, we've just shown the outer fallback. So we'll delay
// showing the inner fallback hoping that B will resolve soon enough.
expect(root).toMatchRenderedOutput('Loading...');

// Wait some more. B is still not resolving.
jest.advanceTimersByTime(500);
Scheduler.unstable_advanceTime(500);
// Give up and render A with a spinner for B.
expect(root).toMatchRenderedOutput('ALoading more...');

// Resolve B.
jest.advanceTimersByTime(500);
Scheduler.unstable_advanceTime(500);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(root).toMatchRenderedOutput('AB');
});

// @gate !enableSyncDefaultUpdates
it(
'interrupts current render when something suspends with a ' +
Expand Down

0 comments on commit c6cca64

Please sign in to comment.