@samitouri / QOS-React-2 / commits / 7ed0706d7e

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