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(() =>