From ddc4b65cfe17b3f08ff9f18f8804ff5b663788c8 Mon Sep 17 00:00:00 2001 From: jddxf <740531372@qq.com> Date: Wed, 8 Apr 2020 04:34:41 +0800 Subject: [PATCH] Clear finished discrete updates during commit phase (#18515) * Reproduce a bug where `flushDiscreteUpdates` causes fallback never to be committed * Ping suspended level when canceling its timer 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. * Clear finished discrete updates during commit phase If a root is finished at a priority lower than that of the latest pending discrete updates on it, these updates must have been finished so we can clear them now. Otherwise, a later call of `flushDiscreteUpdates` would start a new empty render pass which may cause a scheduled timeout to be cancelled. * Add TODO Happened to find this while writing a test. A JSX element comparison failed because one of them elements had a functional component as an owner, which should ever happen. I'll add a regression test later. Co-authored-by: Andrew Clark --- .../src/ReactFiberWorkLoop.js | 29 +++++++++++ ...tSuspenseWithNoopRenderer-test.internal.js | 48 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 98157fbe44..a83f9a6f25 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 716999d697..966fc653ca 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( + <> + + + , + ); + }); });