Remove the warning for setState on unmounted components (#22114)
* Remove warning for setState on unmounted components * Trigger CI
Dan Abramov committed
Aug 18, 2021 at 21:50 UTC
7ed0706d7ec9907e8fd19c4cf0e8625733cf2a1c
4 files changed
+12
-291
packages/react-dom/src/__tests__/ReactCompositeComponent-test.js
+4
-24
@@ -307,7 +307,7 @@ describe('ReactCompositeComponent', () => {
307
ReactDOM.render(<MyComponent />, container2);
308
});
309
310
- it('should warn about `forceUpdate` on unmounted components', () => {
310
+ it('should not warn about `forceUpdate` on unmounted components', () => {
311
const container = document.createElement('div');
312
document.body.appendChild(container);
313
@@ -325,19 +325,11 @@ describe('ReactCompositeComponent', () => {
325
326
ReactDOM.unmountComponentAtNode(container);
327
328
- expect(() => instance.forceUpdate()).toErrorDev(
329
- "Warning: Can't perform a React state update on an unmounted " +
330
- 'component. This is a no-op, but it indicates a memory leak in your ' +
331
- 'application. To fix, cancel all subscriptions and asynchronous ' +
332
- 'tasks in the componentWillUnmount method.\n' +
333
- ' in Component (at **)',
334
- );
335
-
336
- // No additional warning should be recorded
328
+ instance.forceUpdate();
329
instance.forceUpdate();
330
});
331
340
- it('should warn about `setState` on unmounted components', () => {
332
+ it('should not warn about `setState` on unmounted components', () => {
333
const container = document.createElement('div');
334
document.body.appendChild(container);
335
@@ -365,22 +357,10 @@ describe('ReactCompositeComponent', () => {
357
expect(renders).toBe(1);
358
359
instance.setState({value: 1});
368
-
360
expect(renders).toBe(2);
361
362
ReactDOM.render(<div />, container);
372
-
373
- expect(() => {
374
- instance.setState({value: 2});
375
- }).toErrorDev(
376
- "Warning: Can't perform a React state update on an unmounted " +
377
- 'component. This is a no-op, but it indicates a memory leak in your ' +
378
- 'application. To fix, cancel all subscriptions and asynchronous ' +
379
- 'tasks in the componentWillUnmount method.\n' +
380
- ' in Component (at **)\n' +
381
- ' in span',
382
- );
383
-
363
+ instance.setState({value: 2});
364
expect(renders).toBe(2);
365
});
366
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
-103
@@ -12,7 +12,6 @@ import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
import type {Lanes, Lane} from './ReactFiberLane.new';
13
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
14
import type {StackCursor} from './ReactFiberStack.new';
15
-import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
15
import type {Flags} from './ReactFiberFlags';
16
17
import {
@@ -53,10 +52,6 @@ import {
52
scheduleSyncCallback,
53
scheduleLegacySyncCallback,
54
} from './ReactFiberSyncTaskQueue.new';
56
-import {
57
- NoFlags as NoHookEffect,
58
- Passive as HookPassive,
59
-} from './ReactHookEffectTags';
55
import {
56
logCommitStarted,
57
logCommitStopped,
@@ -119,7 +114,6 @@ import {LegacyRoot} from './ReactRootTags';
114
import {
115
NoFlags,
116
Placement,
122
- PassiveStatic,
117
Incomplete,
118
HostEffectMask,
119
Hydrating,
@@ -344,10 +338,6 @@ let nestedPassiveUpdateCount: number = 0;
338
let currentEventTime: number = NoTimestamp;
339
let currentEventTransitionLane: Lanes = NoLanes;
340
347
-// Dev only flag that tracks if passive effects are currently being flushed.
348
-// We warn about state updates for unmounted components differently in this case.
349
-let isFlushingPassiveEffects = false;
350
-
341
export function getWorkInProgressRoot(): FiberRoot | null {
342
return workInProgressRoot;
343
}
@@ -454,7 +444,6 @@ export function scheduleUpdateOnFiber(
444
445
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
446
if (root === null) {
457
- warnAboutUpdateOnUnmountedFiberInDEV(fiber);
447
return null;
448
}
449
@@ -2048,10 +2037,6 @@ function flushPassiveEffectsImpl() {
2037
markPassiveEffectsStarted(lanes);
2038
}
2039
2051
- if (__DEV__) {
2052
- isFlushingPassiveEffects = true;
2053
- }
2054
-
2040
const prevExecutionContext = executionContext;
2041
executionContext |= CommitContext;
2042
@@ -2068,10 +2053,6 @@ function flushPassiveEffectsImpl() {
2053
}
2054
}
2055
2071
- if (__DEV__) {
2072
- isFlushingPassiveEffects = false;
2073
- }
2074
-
2056
if (__DEV__) {
2057
if (enableDebugTracing) {
2058
logPassiveEffectsStopped();
@@ -2503,90 +2484,6 @@ function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
2484
}
2485
}
2486
2506
-let didWarnStateUpdateForUnmountedComponent: Set<string> | null = null;
2507
-function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
2508
- if (__DEV__) {
2509
- const tag = fiber.tag;
2510
- if (
2511
- tag !== HostRoot &&
2512
- tag !== ClassComponent &&
2513
- tag !== FunctionComponent &&
2514
- tag !== ForwardRef &&
2515
- tag !== MemoComponent &&
2516
- tag !== SimpleMemoComponent
2517
- ) {
2518
- // Only warn for user-defined components, not internal ones like Suspense.
2519
- return;
2520
- }
2521
-
2522
- if ((fiber.flags & PassiveStatic) !== NoFlags) {
2523
- const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any);
2524
- if (updateQueue !== null) {
2525
- const lastEffect = updateQueue.lastEffect;
2526
- if (lastEffect !== null) {
2527
- const firstEffect = lastEffect.next;
2528
-
2529
- let effect = firstEffect;
2530
- do {
2531
- if (effect.destroy !== undefined) {
2532
- if ((effect.tag & HookPassive) !== NoHookEffect) {
2533
- return;
2534
- }
2535
- }
2536
- effect = effect.next;
2537
- } while (effect !== firstEffect);
2538
- }
2539
- }
2540
- }
2541
- // We show the whole stack but dedupe on the top component's name because
2542
- // the problematic code almost always lies inside that component.
2543
- const componentName = getComponentNameFromFiber(fiber) || 'ReactComponent';
2544
- if (didWarnStateUpdateForUnmountedComponent !== null) {
2545
- if (didWarnStateUpdateForUnmountedComponent.has(componentName)) {
2546
- return;
2547
- }
2548
- didWarnStateUpdateForUnmountedComponent.add(componentName);
2549
- } else {
2550
- didWarnStateUpdateForUnmountedComponent = new Set([componentName]);
2551
- }
2552
-
2553
- if (isFlushingPassiveEffects) {
2554
- // Do not warn if we are currently flushing passive effects!
2555
- //
2556
- // React can't directly detect a memory leak, but there are some clues that warn about one.
2557
- // One of these clues is when an unmounted React component tries to update its state.
2558
- // For example, if a component forgets to remove an event listener when unmounting,
2559
- // that listener may be called later and try to update state,
2560
- // at which point React would warn about the potential leak.
2561
- //
2562
- // Warning signals are the most useful when they're strong.
2563
- // (So we should avoid false positive warnings.)
2564
- // Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.:
2565
- // 1. Updating an ancestor that a component had registered itself with on mount.
2566
- // 2. Resetting state when a component is hidden after going offscreen.
2567
- } else {
2568
- const previousFiber = ReactCurrentFiberCurrent;
2569
- try {
2570
- setCurrentDebugFiberInDEV(fiber);
2571
- console.error(
2572
- "Can't perform a React state update on an unmounted component. This " +
2573
- 'is a no-op, but it indicates a memory leak in your application. To ' +
2574
- 'fix, cancel all subscriptions and asynchronous tasks in %s.',
2575
- tag === ClassComponent
2576
- ? 'the componentWillUnmount method'
2577
- : 'a useEffect cleanup function',
2578
- );
2579
- } finally {
2580
- if (previousFiber) {
2581
- setCurrentDebugFiberInDEV(fiber);
2582
- } else {
2583
- resetCurrentDebugFiberInDEV();
2584
- }
2585
- }
2586
- }
2587
- }
2588
-}
2589
-
2487
let beginWork;
2488
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
2489
const dummyFiber = null;
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
-103
@@ -12,7 +12,6 @@ import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
import type {Lanes, Lane} from './ReactFiberLane.old';
13
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
14
import type {StackCursor} from './ReactFiberStack.old';
15
-import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
15
import type {Flags} from './ReactFiberFlags';
16
17
import {
@@ -53,10 +52,6 @@ import {
52
scheduleSyncCallback,
53
scheduleLegacySyncCallback,
54
} from './ReactFiberSyncTaskQueue.old';
56
-import {
57
- NoFlags as NoHookEffect,
58
- Passive as HookPassive,
59
-} from './ReactHookEffectTags';
55
import {
56
logCommitStarted,
57
logCommitStopped,
@@ -119,7 +114,6 @@ import {LegacyRoot} from './ReactRootTags';
114
import {
115
NoFlags,
116
Placement,
122
- PassiveStatic,
117
Incomplete,
118
HostEffectMask,
119
Hydrating,
@@ -344,10 +338,6 @@ let nestedPassiveUpdateCount: number = 0;
338
let currentEventTime: number = NoTimestamp;
339
let currentEventTransitionLane: Lanes = NoLanes;
340
347
-// Dev only flag that tracks if passive effects are currently being flushed.
348
-// We warn about state updates for unmounted components differently in this case.
349
-let isFlushingPassiveEffects = false;
350
-
341
export function getWorkInProgressRoot(): FiberRoot | null {
342
return workInProgressRoot;
343
}
@@ -454,7 +444,6 @@ export function scheduleUpdateOnFiber(
444
445
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
446
if (root === null) {
457
- warnAboutUpdateOnUnmountedFiberInDEV(fiber);
447
return null;
448
}
449
@@ -2048,10 +2037,6 @@ function flushPassiveEffectsImpl() {
2037
markPassiveEffectsStarted(lanes);
2038
}
2039
2051
- if (__DEV__) {
2052
- isFlushingPassiveEffects = true;
2053
- }
2054
-
2040
const prevExecutionContext = executionContext;
2041
executionContext |= CommitContext;
2042
@@ -2068,10 +2053,6 @@ function flushPassiveEffectsImpl() {
2053
}
2054
}
2055
2071
- if (__DEV__) {
2072
- isFlushingPassiveEffects = false;
2073
- }
2074
-
2056
if (__DEV__) {
2057
if (enableDebugTracing) {
2058
logPassiveEffectsStopped();
@@ -2503,90 +2484,6 @@ function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
2484
}
2485
}
2486
2506
-let didWarnStateUpdateForUnmountedComponent: Set<string> | null = null;
2507
-function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
2508
- if (__DEV__) {
2509
- const tag = fiber.tag;
2510
- if (
2511
- tag !== HostRoot &&
2512
- tag !== ClassComponent &&
2513
- tag !== FunctionComponent &&
2514
- tag !== ForwardRef &&
2515
- tag !== MemoComponent &&
2516
- tag !== SimpleMemoComponent
2517
- ) {
2518
- // Only warn for user-defined components, not internal ones like Suspense.
2519
- return;
2520
- }
2521
-
2522
- if ((fiber.flags & PassiveStatic) !== NoFlags) {
2523
- const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any);
2524
- if (updateQueue !== null) {
2525
- const lastEffect = updateQueue.lastEffect;
2526
- if (lastEffect !== null) {
2527
- const firstEffect = lastEffect.next;
2528
-
2529
- let effect = firstEffect;
2530
- do {
2531
- if (effect.destroy !== undefined) {
2532
- if ((effect.tag & HookPassive) !== NoHookEffect) {
2533
- return;
2534
- }
2535
- }
2536
- effect = effect.next;
2537
- } while (effect !== firstEffect);
2538
- }
2539
- }
2540
- }
2541
- // We show the whole stack but dedupe on the top component's name because
2542
- // the problematic code almost always lies inside that component.
2543
- const componentName = getComponentNameFromFiber(fiber) || 'ReactComponent';
2544
- if (didWarnStateUpdateForUnmountedComponent !== null) {
2545
- if (didWarnStateUpdateForUnmountedComponent.has(componentName)) {
2546
- return;
2547
- }
2548
- didWarnStateUpdateForUnmountedComponent.add(componentName);
2549
- } else {
2550
- didWarnStateUpdateForUnmountedComponent = new Set([componentName]);
2551
- }
2552
-
2553
- if (isFlushingPassiveEffects) {
2554
- // Do not warn if we are currently flushing passive effects!
2555
- //
2556
- // React can't directly detect a memory leak, but there are some clues that warn about one.
2557
- // One of these clues is when an unmounted React component tries to update its state.
2558
- // For example, if a component forgets to remove an event listener when unmounting,
2559
- // that listener may be called later and try to update state,
2560
- // at which point React would warn about the potential leak.
2561
- //
2562
- // Warning signals are the most useful when they're strong.
2563
- // (So we should avoid false positive warnings.)
2564
- // Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.:
2565
- // 1. Updating an ancestor that a component had registered itself with on mount.
2566
- // 2. Resetting state when a component is hidden after going offscreen.
2567
- } else {
2568
- const previousFiber = ReactCurrentFiberCurrent;
2569
- try {
2570
- setCurrentDebugFiberInDEV(fiber);
2571
- console.error(
2572
- "Can't perform a React state update on an unmounted component. This " +
2573
- 'is a no-op, but it indicates a memory leak in your application. To ' +
2574
- 'fix, cancel all subscriptions and asynchronous tasks in %s.',
2575
- tag === ClassComponent
2576
- ? 'the componentWillUnmount method'
2577
- : 'a useEffect cleanup function',
2578
- );
2579
- } finally {
2580
- if (previousFiber) {
2581
- setCurrentDebugFiberInDEV(fiber);
2582
- } else {
2583
- resetCurrentDebugFiberInDEV();
2584
- }
2585
- }
2586
- }
2587
- }
2588
-}
2589
-
2487
let beginWork;
2488
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
2489
const dummyFiber = null;
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+8
-61
@@ -360,7 +360,7 @@ describe('ReactHooksWithNoopRenderer', () => {
360
expect(firstUpdater).toBe(secondUpdater);
361
});
362
363
- it('warns on set after unmount', () => {
363
+ it('does not warn on set after unmount', () => {
364
let _updateCount;
365
function Counter(props, ref) {
366
const [, updateCount] = useState(0);
@@ -372,49 +372,7 @@ describe('ReactHooksWithNoopRenderer', () => {
372
expect(Scheduler).toFlushWithoutYielding();
373
ReactNoop.render(null);
374
expect(Scheduler).toFlushWithoutYielding();
375
- expect(() => act(() => _updateCount(1))).toErrorDev(
376
- "Warning: Can't perform a React state update on an unmounted " +
377
- 'component. This is a no-op, but it indicates a memory leak in your ' +
378
- 'application. To fix, cancel all subscriptions and asynchronous ' +
379
- 'tasks in a useEffect cleanup function.\n' +
380
- ' in Counter (at **)',
381
- );
382
- });
383
-
384
- it('dedupes the warning by component name', () => {
385
- let _updateCountA;
386
- function CounterA(props, ref) {
387
- const [, updateCount] = useState(0);
388
- _updateCountA = updateCount;
389
- return null;
390
- }
391
- let _updateCountB;
392
- function CounterB(props, ref) {
393
- const [, updateCount] = useState(0);
394
- _updateCountB = updateCount;
395
- return null;
396
- }
397
-
398
- ReactNoop.render([<CounterA key="A" />, <CounterB key="B" />]);
399
- expect(Scheduler).toFlushWithoutYielding();
400
- ReactNoop.render(null);
401
- expect(Scheduler).toFlushWithoutYielding();
402
- expect(() => act(() => _updateCountA(1))).toErrorDev(
403
- "Warning: Can't perform a React state update on an unmounted " +
404
- 'component. This is a no-op, but it indicates a memory leak in your ' +
405
- 'application. To fix, cancel all subscriptions and asynchronous ' +
406
- 'tasks in a useEffect cleanup function.\n' +
407
- ' in CounterA (at **)',
408
- );
409
- // already cached so this logs no error
410
- act(() => _updateCountA(2));
411
- expect(() => act(() => _updateCountB(1))).toErrorDev(
412
- "Warning: Can't perform a React state update on an unmounted " +
413
- 'component. This is a no-op, but it indicates a memory leak in your ' +
414
- 'application. To fix, cancel all subscriptions and asynchronous ' +
415
- 'tasks in a useEffect cleanup function.\n' +
416
- ' in CounterB (at **)',
417
- );
375
+ act(() => _updateCount(1));
376
});
377
378
it('works with memo', () => {
@@ -1401,7 +1359,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1359
});
1360
});
1361
1404
- it('warns about state updates for unmounted components with no pending passive unmounts', () => {
1362
+ it('does not warn about state updates for unmounted components with no pending passive unmounts', () => {
1363
let completePendingRequest = null;
1364
function Component() {
1365
Scheduler.unstable_yieldValue('Component');
@@ -1432,13 +1390,11 @@ describe('ReactHooksWithNoopRenderer', () => {
1390
expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1391
1392
// Simulate an XHR completing.
1435
- expect(completePendingRequest).toErrorDev(
1436
- "Warning: Can't perform a React state update on an unmounted component.",
1437
- );
1393
+ completePendingRequest();
1394
});
1395
});
1396
1441
- it('still warns if there are pending passive unmount effects but not for the current fiber', () => {
1397
+ it('does not warn if there are pending passive unmount effects but not for the current fiber', () => {
1398
let completePendingRequest = null;
1399
function ComponentWithXHR() {
1400
Scheduler.unstable_yieldValue('Component');
@@ -1492,13 +1448,11 @@ describe('ReactHooksWithNoopRenderer', () => {
1448
expect(Scheduler).toFlushAndYieldThrough(['a:layout destroy']);
1449
1450
// Simulate an XHR completing in the component without a pending passive effect..
1495
- expect(completePendingRequest).toErrorDev(
1496
- "Warning: Can't perform a React state update on an unmounted component.",
1497
- );
1451
+ completePendingRequest();
1452
});
1453
});
1454
1501
- it('warns if there are updates after pending passive unmount effects have been flushed', () => {
1455
+ it('does not warn if there are updates after pending passive unmount effects have been flushed', () => {
1456
let updaterFunction;
1457
1458
function Component() {
@@ -1529,14 +1483,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1483
expect(Scheduler).toFlushAndYield(['passive destroy']);
1484
1485
act(() => {
1532
- expect(() => {
1533
- updaterFunction(true);
1534
- }).toErrorDev(
1535
- "Warning: Can't perform a React state update on an unmounted component. " +
1536
- 'This is a no-op, but it indicates a memory leak in your application. ' +
1537
- 'To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.\n' +
1538
- ' in Component (at **)',
1539
- );
1486
+ updaterFunction(true);
1487
});
1488
});
1489