diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 69fc983fb13..bfb2d09a1d8 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -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 { @@ -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) { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 3d66b81c8b6..80bd4efc756 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -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( + }> + + + , + { + 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'); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 017817fd621..6259d798f09 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -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( + }> + + , + ); + 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(() =>