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(
+ <>
+
+
+ >,
+ );
+ });
});