diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 390f46af4ea4c..5cde36aa99e14 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -181,6 +181,7 @@ const {ReactCurrentDispatcher, ReactCurrentOwner} = ReactSharedInternals; let didWarnAboutStateTransition; let didWarnSetStateChildContext; +let suppressUpdateOnUnmountedWarning; let warnAboutUpdateOnUnmounted; let warnAboutInvalidUpdates; @@ -199,6 +200,7 @@ if (enableSchedulerTracing) { if (__DEV__) { didWarnAboutStateTransition = false; didWarnSetStateChildContext = false; + suppressUpdateOnUnmountedWarning = false; const didWarnStateUpdateForUnmountedComponent = {}; warnAboutUpdateOnUnmounted = function(fiber: Fiber, isClass: boolean) { @@ -605,6 +607,12 @@ function flushPassiveEffects() { // We call the scheduled callback instead of commitPassiveEffects directly // to ensure tracing works correctly. passiveEffectCallback(); + if (__DEV__) { + // Flushing passive effects could have led to unmounting a component + // that the user has already called setState on. So temporarily suppress + // any warnings about that until the next scheduleWork call. + suppressUpdateOnUnmountedWarning = true; + } } } @@ -1847,18 +1855,27 @@ export function warnIfNotCurrentlyBatchingInDev(fiber: Fiber): void { function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { const root = scheduleWorkToRoot(fiber, expirationTime); + + let suppressWarning; + if (__DEV__) { + suppressWarning = suppressUpdateOnUnmountedWarning; + suppressUpdateOnUnmountedWarning = false; + } + if (root === null) { if (__DEV__) { - switch (fiber.tag) { - case ClassComponent: - warnAboutUpdateOnUnmounted(fiber, true); - break; - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: - warnAboutUpdateOnUnmounted(fiber, false); - break; + if (!suppressWarning) { + switch (fiber.tag) { + case ClassComponent: + warnAboutUpdateOnUnmounted(fiber, true); + break; + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + warnAboutUpdateOnUnmounted(fiber, false); + break; + } } } return; diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 087c8572cd66e..e2b62b0922762 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -1699,6 +1699,74 @@ describe('ReactHooks', () => { ).toThrow('Hello'); }); + it('does not fire a false positive warning when previous effect unmounts the component', () => { + let {useState, useEffect} = React; + let globalListener; + let _setState; + + function A() { + const [show, setShow] = useState(true); + function hideMe() { + setShow(false); + } + return show ? : null; + } + + function B(props) { + return ; + } + + function C({hideMe}) { + const [, setState] = useState(); + + useEffect(() => { + let isStale = false; + + _setState = setState; + globalListener = () => { + if (!isStale) { + setState('hello'); + } + if (!isStale) { + setState('goodbye'); + } + if (!isStale) { + setState('hello'); + } + }; + + return () => { + isStale = true; + hideMe(); + }; + }); + return null; + } + + ReactTestRenderer.act(() => { + ReactTestRenderer.create(); + }); + + expect(() => { + globalListener(); + globalListener(); + }).toWarnDev([ + 'An update to C inside a test was not wrapped in act', + 'An update to C inside a test was not wrapped in act', + // Note: should *not* warn about updates on unmounted component. + // Because there's no way for component to know it got unmounted. + ]); + + // Verify the warning isn't disabled permanently. + expect(() => { + ReactTestRenderer.act(() => { + _setState('bad'); + }); + }).toWarnDev([ + "Can't perform a React state update on an unmounted component", + ]); + }); + // Regression test for https://github.com/facebook/react/issues/14790 it('does not fire a false positive warning when suspending memo', async () => { const {Suspense, useState} = React;