mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fix incorrect unmounted state update warning (#18617)
* Fix incorrect unmounted state update warning We detach fibers (which nulls the field) when we commit a deletion, so any state updates scheduled between that point and when we eventually flush passive effect destroys won't have a way to check if there is a pending passive unmount effect scheduled for its alternate unless we also explicitly track this for both the current and the alternate. This commit adds a new DEV-only effect type, `PendingPassiveUnmountDev`, to handle this case.
This commit is contained in:
@@ -116,6 +116,7 @@ import {
|
||||
Snapshot,
|
||||
Callback,
|
||||
Passive,
|
||||
PassiveUnmountPendingDev,
|
||||
Incomplete,
|
||||
HostEffectMask,
|
||||
Hydrating,
|
||||
@@ -2310,6 +2311,15 @@ export function enqueuePendingPassiveHookEffectUnmount(
|
||||
): void {
|
||||
if (runAllPassiveEffectDestroysBeforeCreates) {
|
||||
pendingPassiveHookEffectsUnmount.push(effect, fiber);
|
||||
if (__DEV__) {
|
||||
if (deferPassiveEffectCleanupDuringUnmount) {
|
||||
fiber.effectTag |= PassiveUnmountPendingDev;
|
||||
const alternate = fiber.alternate;
|
||||
if (alternate !== null) {
|
||||
alternate.effectTag |= PassiveUnmountPendingDev;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rootDoesHavePassiveEffects) {
|
||||
rootDoesHavePassiveEffects = true;
|
||||
scheduleCallback(NormalPriority, () => {
|
||||
@@ -2372,6 +2382,17 @@ function flushPassiveEffectsImpl() {
|
||||
const fiber = ((unmountEffects[i + 1]: any): Fiber);
|
||||
const destroy = effect.destroy;
|
||||
effect.destroy = undefined;
|
||||
|
||||
if (__DEV__) {
|
||||
if (deferPassiveEffectCleanupDuringUnmount) {
|
||||
fiber.effectTag &= ~PassiveUnmountPendingDev;
|
||||
const alternate = fiber.alternate;
|
||||
if (alternate !== null) {
|
||||
alternate.effectTag &= ~PassiveUnmountPendingDev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof destroy === 'function') {
|
||||
if (__DEV__) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
@@ -2851,7 +2872,7 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
|
||||
) {
|
||||
// If there are pending passive effects unmounts for this Fiber,
|
||||
// we can assume that they would have prevented this update.
|
||||
if (pendingPassiveHookEffectsUnmount.indexOf(fiber) >= 0) {
|
||||
if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
Snapshot,
|
||||
Callback,
|
||||
Passive,
|
||||
PassiveUnmountPendingDev,
|
||||
Incomplete,
|
||||
HostEffectMask,
|
||||
Hydrating,
|
||||
@@ -2329,6 +2330,15 @@ export function enqueuePendingPassiveHookEffectUnmount(
|
||||
): void {
|
||||
if (runAllPassiveEffectDestroysBeforeCreates) {
|
||||
pendingPassiveHookEffectsUnmount.push(effect, fiber);
|
||||
if (__DEV__) {
|
||||
if (deferPassiveEffectCleanupDuringUnmount) {
|
||||
fiber.effectTag |= PassiveUnmountPendingDev;
|
||||
const alternate = fiber.alternate;
|
||||
if (alternate !== null) {
|
||||
alternate.effectTag |= PassiveUnmountPendingDev;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rootDoesHavePassiveEffects) {
|
||||
rootDoesHavePassiveEffects = true;
|
||||
scheduleCallback(NormalPriority, () => {
|
||||
@@ -2391,6 +2401,17 @@ function flushPassiveEffectsImpl() {
|
||||
const fiber = ((unmountEffects[i + 1]: any): Fiber);
|
||||
const destroy = effect.destroy;
|
||||
effect.destroy = undefined;
|
||||
|
||||
if (__DEV__) {
|
||||
if (deferPassiveEffectCleanupDuringUnmount) {
|
||||
fiber.effectTag &= ~PassiveUnmountPendingDev;
|
||||
const alternate = fiber.alternate;
|
||||
if (alternate !== null) {
|
||||
alternate.effectTag &= ~PassiveUnmountPendingDev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof destroy === 'function') {
|
||||
if (__DEV__) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
@@ -2873,7 +2894,7 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
|
||||
) {
|
||||
// If there are pending passive effects unmounts for this Fiber,
|
||||
// we can assume that they would have prevented this update.
|
||||
if (pendingPassiveHookEffectsUnmount.indexOf(fiber) >= 0) {
|
||||
if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-18
@@ -10,28 +10,29 @@
|
||||
export type SideEffectTag = number;
|
||||
|
||||
// Don't change these two values. They're used by React Dev Tools.
|
||||
export const NoEffect = /* */ 0b0000000000000;
|
||||
export const PerformedWork = /* */ 0b0000000000001;
|
||||
export const NoEffect = /* */ 0b00000000000000;
|
||||
export const PerformedWork = /* */ 0b00000000000001;
|
||||
|
||||
// You can change the rest (and add more).
|
||||
export const Placement = /* */ 0b0000000000010;
|
||||
export const Update = /* */ 0b0000000000100;
|
||||
export const PlacementAndUpdate = /* */ 0b0000000000110;
|
||||
export const Deletion = /* */ 0b0000000001000;
|
||||
export const ContentReset = /* */ 0b0000000010000;
|
||||
export const Callback = /* */ 0b0000000100000;
|
||||
export const DidCapture = /* */ 0b0000001000000;
|
||||
export const Ref = /* */ 0b0000010000000;
|
||||
export const Snapshot = /* */ 0b0000100000000;
|
||||
export const Passive = /* */ 0b0001000000000;
|
||||
export const Hydrating = /* */ 0b0010000000000;
|
||||
export const HydratingAndUpdate = /* */ 0b0010000000100;
|
||||
export const Placement = /* */ 0b00000000000010;
|
||||
export const Update = /* */ 0b00000000000100;
|
||||
export const PlacementAndUpdate = /* */ 0b00000000000110;
|
||||
export const Deletion = /* */ 0b00000000001000;
|
||||
export const ContentReset = /* */ 0b00000000010000;
|
||||
export const Callback = /* */ 0b00000000100000;
|
||||
export const DidCapture = /* */ 0b00000001000000;
|
||||
export const Ref = /* */ 0b00000010000000;
|
||||
export const Snapshot = /* */ 0b00000100000000;
|
||||
export const Passive = /* */ 0b00001000000000;
|
||||
export const PassiveUnmountPendingDev = /* */ 0b10000000000000;
|
||||
export const Hydrating = /* */ 0b00010000000000;
|
||||
export const HydratingAndUpdate = /* */ 0b00010000000100;
|
||||
|
||||
// Passive & Update & Callback & Ref & Snapshot
|
||||
export const LifecycleEffectMask = /* */ 0b0001110100100;
|
||||
export const LifecycleEffectMask = /* */ 0b00001110100100;
|
||||
|
||||
// Union of all host effects
|
||||
export const HostEffectMask = /* */ 0b0011111111111;
|
||||
export const HostEffectMask = /* */ 0b00011111111111;
|
||||
|
||||
export const Incomplete = /* */ 0b0100000000000;
|
||||
export const ShouldCapture = /* */ 0b1000000000000;
|
||||
export const Incomplete = /* */ 0b00100000000000;
|
||||
export const ShouldCapture = /* */ 0b01000000000000;
|
||||
|
||||
@@ -1252,6 +1252,95 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
|
||||
it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', () => {
|
||||
let setParentState = null;
|
||||
const setChildStates = [];
|
||||
|
||||
function Parent() {
|
||||
const [state, setState] = useState(true);
|
||||
setParentState = setState;
|
||||
Scheduler.unstable_yieldValue(`Parent ${state} render`);
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.unstable_yieldValue(`Parent ${state} commit`);
|
||||
});
|
||||
if (state) {
|
||||
return (
|
||||
<>
|
||||
<Child label="one" />
|
||||
<Child label="two" />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function Child({label}) {
|
||||
const [state, setState] = useState(0);
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.unstable_yieldValue(`Child ${label} commit`);
|
||||
});
|
||||
useEffect(() => {
|
||||
setChildStates.push(setState);
|
||||
Scheduler.unstable_yieldValue(`Child ${label} passive create`);
|
||||
return () => {
|
||||
Scheduler.unstable_yieldValue(`Child ${label} passive destroy`);
|
||||
};
|
||||
}, []);
|
||||
Scheduler.unstable_yieldValue(`Child ${label} render`);
|
||||
return state;
|
||||
}
|
||||
|
||||
// Schedule debounced state update for child (prob a no-op for this test)
|
||||
// later tick: schedule unmount for parent
|
||||
// start process unmount (but don't flush passive effectS)
|
||||
// State update on child
|
||||
act(() => {
|
||||
ReactNoop.render(<Parent />);
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Parent true render',
|
||||
'Child one render',
|
||||
'Child two render',
|
||||
'Child one commit',
|
||||
'Child two commit',
|
||||
'Parent true commit',
|
||||
'Child one passive create',
|
||||
'Child two passive create',
|
||||
]);
|
||||
|
||||
// Update children.
|
||||
setChildStates.forEach(setChildState => setChildState(1));
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Child one render',
|
||||
'Child two render',
|
||||
'Child one commit',
|
||||
'Child two commit',
|
||||
]);
|
||||
|
||||
// Schedule another update for children, and partially process it.
|
||||
setChildStates.forEach(setChildState => setChildState(2));
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Child one render']);
|
||||
|
||||
// Schedule unmount for the parent that unmounts children with pending update.
|
||||
Scheduler.unstable_runWithPriority(
|
||||
Scheduler.unstable_UserBlockingPriority,
|
||||
() => setParentState(false),
|
||||
);
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Parent false render',
|
||||
'Parent false commit',
|
||||
]);
|
||||
|
||||
// Schedule updates for children too (which should be ignored)
|
||||
setChildStates.forEach(setChildState => setChildState(2));
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Child one passive destroy',
|
||||
'Child two passive destroy',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('warns about state updates for unmounted components with no pending passive unmounts', () => {
|
||||
let completePendingRequest = null;
|
||||
function Component() {
|
||||
|
||||
Reference in New Issue
Block a user