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
10 changes: 9 additions & 1 deletion packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ import {
computeAsyncExpiration,
computeInteractiveExpiration,
} from './ReactFiberExpirationTime';
import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode';
import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
import {createCapturedValue} from './ReactCapturedValue';
import {
Expand Down Expand Up @@ -1597,6 +1597,14 @@ function retrySuspendedRoot(
markPendingPriorityLevel(root, retryTime);
}

if ((fiber.mode & ConcurrentMode) !== NoContext) {
if (root === nextRoot && nextRenderExpirationTime === suspendedTime) {
// Received a ping at the same priority level at which we're currently
// rendering. Restart from the root.
nextRoot = null;
}
}

scheduleWorkToRoot(fiber, retryTime);
const rootExpirationTime = root.expirationTime;
if (rootExpirationTime !== NoWork) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,57 @@ describe('ReactSuspense', () => {
expect(root).toFlushAndYield(['B']);
expect(root).toMatchRenderedOutput('AB');
});

it('interrupts current render if promise resolves before current render phase', () => {
let didResolve = false;
let listeners = [];

const thenable = {
then(resolve) {
if (!didResolve) {
listeners.push(resolve);
} else {
resolve();
}
},
};

function resolveThenable() {
didResolve = true;
listeners.forEach(l => l());
}

function Async() {
if (!didResolve) {
ReactTestRenderer.unstable_yield('Suspend!');
throw thenable;
}
ReactTestRenderer.unstable_yield('Async');
return 'Async';
}

const root = ReactTestRenderer.create(
<Placeholder delayMs={1000} fallback={<Text text="Loading..." />}>
<Async />
<Text text="Sibling" />
</Placeholder>,
{
unstable_isConcurrent: true,
},
);

expect(root).toFlushAndYieldThrough(['Suspend!']);

// The promise resolves before the current render phase has completed
resolveThenable();
expect(ReactTestRenderer).toHaveYielded([]);

// Start over from the root, instead of continuing.
expect(root).toFlushAndYield([
// Async renders again *before* Sibling
'Async',
'Sibling',
]);
expect(root).toMatchRenderedOutput('AsyncSibling');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});

it('resolves successfully even if fallback render is pending', async () => {
ReactNoop.render(
<Placeholder delayMs={1000} fallback={<Text text="Loading..." />}>
<AsyncText text="Async" ms={3000} />
</Placeholder>,
);
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
await advanceTimers(1500);
expect(ReactNoop.expire(1500)).toEqual([]);
// Before we have a chance to flush, the promise resolves.
await advanceTimers(2000);
expect(ReactNoop.clearYields()).toEqual(['Promise resolved [Async]']);
expect(ReactNoop.flush()).toEqual(['Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
});

it('throws a helpful error when an update is suspends without a placeholder', () => {
expect(() => {
ReactNoop.flushSync(() =>
Expand Down