diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 98157fbe44e4..a83f9a6f2505 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1168,6 +1168,19 @@ function prepareFreshStack(root, expirationTime) { cancelTimeout(timeoutHandle); } + // Check if there's a suspended level at lower priority. + const lastSuspendedTime = root.lastSuspendedTime; + if (lastSuspendedTime !== NoWork && lastSuspendedTime < expirationTime) { + const lastPingedTime = root.lastPingedTime; + // Make sure the suspended level is marked as pinged so that we return back + // to it later, in case the render we're about to start gets aborted. + // Generally we only reach this path via a ping, but we shouldn't assume + // that will always be the case. + if (lastPingedTime === NoWork || lastPingedTime > lastSuspendedTime) { + root.lastPingedTime = lastSuspendedTime; + } + } + if (workInProgress !== null) { let interruptedWork = workInProgress.return; while (interruptedWork !== null) { @@ -1202,6 +1215,9 @@ function handleError(root, thrownValue) { resetContextDependencies(); resetHooksAfterThrow(); resetCurrentDebugFiberInDEV(); + // TODO: I found and added this missing line while investigating a + // separate issue. Write a regression test using string refs. + ReactCurrentOwner.current = null; if (workInProgress === null || workInProgress.return === null) { // Expected to be working on a non-root fiber. This is a fatal error @@ -1769,6 +1785,19 @@ function commitRootImpl(root, renderPriorityLevel) { remainingExpirationTimeBeforeCommit, ); + // Clear already finished discrete updates in case that a later call of + // `flushDiscreteUpdates` starts a useless render pass which may cancels + // a scheduled timeout. + if (rootsWithPendingDiscreteUpdates !== null) { + const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); + if ( + lastDiscreteTime !== undefined && + remainingExpirationTimeBeforeCommit < lastDiscreteTime + ) { + rootsWithPendingDiscreteUpdates.delete(root); + } + } + if (root === workInProgressRoot) { // We can reset these now that they are finished. workInProgressRoot = null; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 716999d697a5..966fc653ca12 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -3597,4 +3597,52 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); }); + + it('regression: empty render at high priority causes update to be dropped', async () => { + // Reproduces a bug where flushDiscreteUpdates starts a new (empty) render + // pass which cancels a scheduled timeout and causes the fallback never to + // be committed. + function App({text, shouldSuspend}) { + return ( + <> + + }> + {shouldSuspend && } + + + ); + } + + const root = ReactNoop.createRoot(); + ReactNoop.discreteUpdates(() => { + // High pri + root.render(); + }); + // Low pri + root.render(); + + expect(Scheduler).toFlushAndYield([ + // Render the high pri update + 'A', + // Render the low pri update + 'A', + 'Suspend! [B]', + 'Loading...', + ]); + expect(root).toMatchRenderedOutput(); + + // Triggers erstwhile bug where flushDiscreteUpdates caused an empty render + // at a previously committed level + ReactNoop.flushDiscreteUpdates(); + + // Commit the placeholder + Scheduler.unstable_advanceTime(2000); + await advanceTimers(2000); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + }); });