From fad5102101e4b34bbd004a9044ae5e46581231ed Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 26 Sep 2019 12:47:48 -0700 Subject: [PATCH] [bugfix] Fix false positive render phase update (#16907) Need to reset the current "debug phase" inside the catch block. Otherwise React thinks we're still in the render phase during the subsequent event. --- .../src/ReactFiberWorkLoop.js | 13 +++-- ...tSuspenseWithNoopRenderer-test.internal.js | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 389ff45ff3232..4ea0ccfa3fabf 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1329,6 +1329,7 @@ function handleError(root, thrownValue) { // Reset module-level state that was set during the render phase. resetContextDependencies(); resetHooks(); + resetCurrentDebugFiberInDEV(); if (workInProgress === null || workInProgress.return === null) { // Expected to be working on a non-root fiber. This is a fatal error @@ -2672,10 +2673,12 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { throw originalError; } - // Keep this code in sync with renderRoot; any changes here must have + // Keep this code in sync with handleError; any changes here must have // corresponding changes there. resetContextDependencies(); resetHooks(); + // Don't reset current debug fiber, since we're about to work on the + // same fiber again. // Unwind the failed stack frame unwindInterruptedWork(unitOfWork); @@ -3072,10 +3075,10 @@ function startWorkOnPendingInteractions(root, expirationTime) { ); // Store the current set of interactions on the FiberRoot for a few reasons: - // We can re-use it in hot functions like renderRoot() without having to - // recalculate it. We will also use it in commitWork() to pass to any Profiler - // onRender() hooks. This also provides DevTools with a way to access it when - // the onCommitRoot() hook is called. + // We can re-use it in hot functions like performConcurrentWorkOnRoot() + // without having to recalculate it. We will also use it in commitWork() to + // pass to any Profiler onRender() hooks. This also provides DevTools with a + // way to access it when the onCommitRoot() hook is called. root.memoizedInteractions = interactions; if (interactions.size > 0) { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 6ed257726d8c9..5a78700860e6c 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -2576,4 +2576,53 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(root).toMatchRenderedOutput(); }); }); + + it('regression test: resets current "debug phase" after suspending', async () => { + function App() { + return ( + + + + ); + } + + const thenable = {then() {}}; + + let foo; + class Foo extends React.Component { + state = {suspend: false}; + render() { + foo = this; + + if (this.state.suspend) { + Scheduler.unstable_yieldValue('Suspend!'); + throw thenable; + } + + return ; + } + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + }); + + expect(Scheduler).toHaveYielded(['Foo']); + + await ReactNoop.act(async () => { + foo.setState({suspend: true}); + + // In the regression that this covers, we would neglect to reset the + // current debug phase after suspending (in the catch block), so React + // thinks we're still inside the render phase. + expect(Scheduler).toFlushAndYieldThrough(['Suspend!']); + + // Then when this setState happens, React would incorrectly fire a warning + // about updates that happen the render phase (only fired by classes). + foo.setState({suspend: false}); + }); + + expect(root).toMatchRenderedOutput(); + }); });