diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 0a6ae9df7bc..5f85952a9b2 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -949,27 +949,19 @@ function updatePlaceholderComponent( // Check if we already attempted to render the normal state. If we did, // and we timed out, render the placeholder state. - const alreadyCaptured = - (workInProgress.effectTag & DidCapture) === NoEffect; + const nextDidTimeout = workInProgress.updateQueue !== null; - let nextDidTimeout; - if (current !== null && workInProgress.updateQueue !== null) { - if (enableSchedulerTracing) { + if ((workInProgress.mode & StrictMode) === NoEffect) { + if (enableSchedulerTracing && nextDidTimeout) { // Handle special case of rendering a Placeholder for a sync, suspended tree. // We flag this to properly trace and count interactions. // Otherwise interaction pending count will be decremented too many times. captureWillSyncRenderPlaceholder(); } - - // We're outside strict mode. Something inside this Placeholder boundary - // suspended during the last commit. Switch to the placholder. + // The next time we render, we try the primary children again even if no + // promise has timed out. workInProgress.updateQueue = null; - nextDidTimeout = true; } else { - nextDidTimeout = !alreadyCaptured; - } - - if ((workInProgress.mode & StrictMode) !== NoEffect) { if (nextDidTimeout) { // If the timed-out view commits, schedule an update effect to record // the committed time. diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 53993f4136a..b24f9a43d42 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -356,11 +356,7 @@ function commitLifeCycles( if (enableSuspense) { if ((finishedWork.mode & StrictMode) === NoEffect) { // In loose mode, a placeholder times out by scheduling a synchronous - // update in the commit phase. Use `updateQueue` field to signal that - // the Timeout needs to switch to the placeholder. We don't need an - // entire queue. Any non-null value works. - // $FlowFixMe - Intentionally using a value other than an UpdateQueue. - finishedWork.updateQueue = emptyObject; + // update in the commit phase. scheduleWork(finishedWork, Sync); } else { // In strict mode, the Update effect is used to record the time at diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 69fc983fb13..8ad004b5263 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -1597,6 +1597,12 @@ function retrySuspendedRoot( markPendingPriorityLevel(root, retryTime); } + // Mark that we should try rendering the primary children again. + fiber.updateQueue = null; + if (fiber.alternate !== null) { + fiber.alternate.updateQueue = null; + } + scheduleWorkToRoot(fiber, retryTime); const rootExpirationTime = root.expirationTime; if (rootExpirationTime !== NoWork) { diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index bba16ed0b64..f58d1658fc7 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -72,6 +72,8 @@ import { import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority'; import {reconcileChildren} from './ReactFiberBeginWork'; +const emptyObject = {}; + function NoopComponent() { return null; } @@ -232,6 +234,12 @@ function throwException( ); thenable.then(onResolveOrReject, onResolveOrReject); + // Use `updateQueue` field to signal that the Timeout needs to switch + // to the placeholder. We don't need an entire queue. Any non-null + // value works. + // $FlowFixMe - Intentionally using a value other than an UpdateQueue. + workInProgress.updateQueue = emptyObject; + // If the boundary is outside of strict mode, we should *not* suspend // the commit. Pretend as if the suspended component rendered null and // keep rendering. In the commit phase, we'll schedule a subsequent diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 017817fd621..3c766b276de 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -649,6 +649,25 @@ 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(() =>