Skip to content

Commit

Permalink
Remove state update warning for passive effect cleanup functions (#18453
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Brian Vaughn authored Apr 1, 2020
1 parent d8d2b6e commit 5ee0efe
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 45 deletions.
37 changes: 9 additions & 28 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ import {
import getComponentName from 'shared/getComponentName';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {
current as currentDebugFiberInDEV,
isRendering as ReactCurrentDebugFiberIsRenderingInDEV,
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
Expand Down Expand Up @@ -2736,38 +2735,20 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
didWarnStateUpdateForUnmountedComponent = new Set([componentName]);
}

// If we are currently flushing passive effects, change the warning text.
if (isFlushingPassiveEffects) {
// Do not warn if we are currently flushing passive effects!
//
// React can't directly detect a memory leak, but there are some clues that warn about one.
// One of these clues is when an unmounted React component tries to update its state.
// For example, if a component forgets to remove an event listener when unmounting,
// that listener may be called later and try to update state, at which point React would warn about the potential leak.
//
// Warning signals like this are more useful if they're strong.
// For this reason, it's good to always avoid updating state from inside of an effect's cleanup function.
// Even when you know there is no potential leak, React has no way to know and so it will warn anyway.
// In most cases we suggest moving state updates to the useEffect() body instead.
// This works so long as the component is updating its own state (or the state of a descendant).
// that listener may be called later and try to update state,
// at which point React would warn about the potential leak.
//
// However this will not work when a component updates its parent state in a cleanup function.
// If such a component is unmounted but its parent remains mounted, the state will be out of sync.
// For this reason, we avoid showing the warning if a component is updating an ancestor.
let currentTargetFiber = fiber;
while (currentTargetFiber !== null) {
// currentDebugFiberInDEV owns the effect destroy function currently being invoked.
if (
currentDebugFiberInDEV === currentTargetFiber ||
currentDebugFiberInDEV === currentTargetFiber.alternate
) {
console.error(
"Can't perform a React state update from within a useEffect cleanup function. " +
'To fix, move state updates to the useEffect() body.%s',
getStackByFiberInDevAndProd(((currentDebugFiberInDEV: any): Fiber)),
);
break;
}
currentTargetFiber = currentTargetFiber.return;
}
// Warning signals are the most useful when they're strong.
// (So we should avoid false positive warnings.)
// Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.:
// 1. Updating an ancestor that a component had registered itself with on mount.
// 2. Resetting state when a component is hidden after going offscreen.
} else {
console.error(
"Can't perform a React state update on an unmounted component. This " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1307,7 +1307,49 @@ describe('ReactHooksWithNoopRenderer', () => {
});
});

it('shows a warning when a component updates its own state from within passive unmount function', () => {
it('still warns if there are updates after pending passive unmount effects have been flushed', () => {
let updaterFunction;

function Component() {
Scheduler.unstable_yieldValue('Component');
const [state, setState] = React.useState(false);
updaterFunction = setState;
React.useEffect(() => {
Scheduler.unstable_yieldValue('passive create');
return () => {
Scheduler.unstable_yieldValue('passive destroy');
};
}, []);
return state;
}

act(() => {
ReactNoop.renderToRootWithID(<Component />, 'root', () =>
Scheduler.unstable_yieldValue('Sync effect'),
);
});
expect(Scheduler).toHaveYielded([
'Component',
'Sync effect',
'passive create',
]);

ReactNoop.unmountRootWithID('root');
expect(Scheduler).toFlushAndYield(['passive destroy']);

act(() => {
expect(() => {
updaterFunction(true);
}).toErrorDev(
"Warning: Can't perform a React state update on an unmounted component. " +
'This is a no-op, but it indicates a memory leak in your application. ' +
'To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.\n' +
' in Component (at **)',
);
});
});

it('does not show a warning when a component updates its own state from within passive unmount function', () => {
function Component() {
Scheduler.unstable_yieldValue('Component');
const [didLoad, setDidLoad] = React.useState(false);
Expand All @@ -1333,17 +1375,11 @@ describe('ReactHooksWithNoopRenderer', () => {

// Unmount but don't process pending passive destroy function
ReactNoop.unmountRootWithID('root');
expect(() => {
expect(Scheduler).toFlushAndYield(['passive destroy']);
}).toErrorDev(
"Warning: Can't perform a React state update from within a useEffect cleanup function. " +
'To fix, move state updates to the useEffect() body.\n' +
' in Component (at **)',
);
expect(Scheduler).toFlushAndYield(['passive destroy']);
});
});

it('shows a warning when a component updates a childs state from within passive unmount function', () => {
it('does not show a warning when a component updates a childs state from within passive unmount function', () => {
function Parent() {
Scheduler.unstable_yieldValue('Parent');
const updaterRef = React.useRef(null);
Expand Down Expand Up @@ -1378,13 +1414,7 @@ describe('ReactHooksWithNoopRenderer', () => {

// Unmount but don't process pending passive destroy function
ReactNoop.unmountRootWithID('root');
expect(() => {
expect(Scheduler).toFlushAndYield(['Parent passive destroy']);
}).toErrorDev(
"Warning: Can't perform a React state update from within a useEffect cleanup function. " +
'To fix, move state updates to the useEffect() body.\n' +
' in Parent (at **)',
);
expect(Scheduler).toFlushAndYield(['Parent passive destroy']);
});
});

Expand Down
2 changes: 1 addition & 1 deletion scripts/jest/matchers/toWarnDev.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const jestDiff = require('jest-diff').default;
const jestDiff = require('jest-diff');
const util = require('util');
const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError');

Expand Down

0 comments on commit 5ee0efe

Please sign in to comment.