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.
Brian Vaughn committed
Apr 18, 2020 at 12:18 UTC
263bc5d54b6c9bde51328aa8c7ddd985f97db72d
4 files changed
+152
-20
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+22
-1
@@ -116,6 +116,7 @@ import {
116
Snapshot,
117
Callback,
118
Passive,
119
+ PassiveUnmountPendingDev,
120
Incomplete,
121
HostEffectMask,
122
Hydrating,
@@ -2310,6 +2311,15 @@ export function enqueuePendingPassiveHookEffectUnmount(
2311
): void {
2312
if (runAllPassiveEffectDestroysBeforeCreates) {
2313
pendingPassiveHookEffectsUnmount.push(effect, fiber);
2314
+ if (__DEV__) {
2315
+ if (deferPassiveEffectCleanupDuringUnmount) {
2316
+ fiber.effectTag |= PassiveUnmountPendingDev;
2317
+ const alternate = fiber.alternate;
2318
+ if (alternate !== null) {
2319
+ alternate.effectTag |= PassiveUnmountPendingDev;
2320
+ }
2321
+ }
2322
+ }
2323
if (!rootDoesHavePassiveEffects) {
2324
rootDoesHavePassiveEffects = true;
2325
scheduleCallback(NormalPriority, () => {
@@ -2372,6 +2382,17 @@ function flushPassiveEffectsImpl() {
2382
const fiber = ((unmountEffects[i + 1]: any): Fiber);
2383
const destroy = effect.destroy;
2384
effect.destroy = undefined;
2385
+
2386
+ if (__DEV__) {
2387
+ if (deferPassiveEffectCleanupDuringUnmount) {
2388
+ fiber.effectTag &= ~PassiveUnmountPendingDev;
2389
+ const alternate = fiber.alternate;
2390
+ if (alternate !== null) {
2391
+ alternate.effectTag &= ~PassiveUnmountPendingDev;
2392
+ }
2393
+ }
2394
+ }
2395
+
2396
if (typeof destroy === 'function') {
2397
if (__DEV__) {
2398
setCurrentDebugFiberInDEV(fiber);
@@ -2851,7 +2872,7 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
2872
) {
2873
// If there are pending passive effects unmounts for this Fiber,
2874
// we can assume that they would have prevented this update.
2854
- if (pendingPassiveHookEffectsUnmount.indexOf(fiber) >= 0) {
2875
+ if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) {
2876
return;
2877
}
2878
}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+22
-1
@@ -116,6 +116,7 @@ import {
116
Snapshot,
117
Callback,
118
Passive,
119
+ PassiveUnmountPendingDev,
120
Incomplete,
121
HostEffectMask,
122
Hydrating,
@@ -2329,6 +2330,15 @@ export function enqueuePendingPassiveHookEffectUnmount(
2330
): void {
2331
if (runAllPassiveEffectDestroysBeforeCreates) {
2332
pendingPassiveHookEffectsUnmount.push(effect, fiber);
2333
+ if (__DEV__) {
2334
+ if (deferPassiveEffectCleanupDuringUnmount) {
2335
+ fiber.effectTag |= PassiveUnmountPendingDev;
2336
+ const alternate = fiber.alternate;
2337
+ if (alternate !== null) {
2338
+ alternate.effectTag |= PassiveUnmountPendingDev;
2339
+ }
2340
+ }
2341
+ }
2342
if (!rootDoesHavePassiveEffects) {
2343
rootDoesHavePassiveEffects = true;
2344
scheduleCallback(NormalPriority, () => {
@@ -2391,6 +2401,17 @@ function flushPassiveEffectsImpl() {
2401
const fiber = ((unmountEffects[i + 1]: any): Fiber);
2402
const destroy = effect.destroy;
2403
effect.destroy = undefined;
2404
+
2405
+ if (__DEV__) {
2406
+ if (deferPassiveEffectCleanupDuringUnmount) {
2407
+ fiber.effectTag &= ~PassiveUnmountPendingDev;
2408
+ const alternate = fiber.alternate;
2409
+ if (alternate !== null) {
2410
+ alternate.effectTag &= ~PassiveUnmountPendingDev;
2411
+ }
2412
+ }
2413
+ }
2414
+
2415
if (typeof destroy === 'function') {
2416
if (__DEV__) {
2417
setCurrentDebugFiberInDEV(fiber);
@@ -2873,7 +2894,7 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
2894
) {
2895
// If there are pending passive effects unmounts for this Fiber,
2896
// we can assume that they would have prevented this update.
2876
- if (pendingPassiveHookEffectsUnmount.indexOf(fiber) >= 0) {
2897
+ if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) {
2898
return;
2899
}
2900
}
packages/react-reconciler/src/ReactSideEffectTags.js
+19
-18
@@ -10,28 +10,29 @@
10
export type SideEffectTag = number;
11
12
// Don't change these two values. They're used by React Dev Tools.
13
-export const NoEffect = /* */ 0b0000000000000;
14
-export const PerformedWork = /* */ 0b0000000000001;
13
+export const NoEffect = /* */ 0b00000000000000;
14
+export const PerformedWork = /* */ 0b00000000000001;
15
16
// You can change the rest (and add more).
17
-export const Placement = /* */ 0b0000000000010;
18
-export const Update = /* */ 0b0000000000100;
19
-export const PlacementAndUpdate = /* */ 0b0000000000110;
20
-export const Deletion = /* */ 0b0000000001000;
21
-export const ContentReset = /* */ 0b0000000010000;
22
-export const Callback = /* */ 0b0000000100000;
23
-export const DidCapture = /* */ 0b0000001000000;
24
-export const Ref = /* */ 0b0000010000000;
25
-export const Snapshot = /* */ 0b0000100000000;
26
-export const Passive = /* */ 0b0001000000000;
27
-export const Hydrating = /* */ 0b0010000000000;
28
-export const HydratingAndUpdate = /* */ 0b0010000000100;
17
+export const Placement = /* */ 0b00000000000010;
18
+export const Update = /* */ 0b00000000000100;
19
+export const PlacementAndUpdate = /* */ 0b00000000000110;
20
+export const Deletion = /* */ 0b00000000001000;
21
+export const ContentReset = /* */ 0b00000000010000;
22
+export const Callback = /* */ 0b00000000100000;
23
+export const DidCapture = /* */ 0b00000001000000;
24
+export const Ref = /* */ 0b00000010000000;
25
+export const Snapshot = /* */ 0b00000100000000;
26
+export const Passive = /* */ 0b00001000000000;
27
+export const PassiveUnmountPendingDev = /* */ 0b10000000000000;
28
+export const Hydrating = /* */ 0b00010000000000;
29
+export const HydratingAndUpdate = /* */ 0b00010000000100;
30
31
// Passive & Update & Callback & Ref & Snapshot
31
-export const LifecycleEffectMask = /* */ 0b0001110100100;
32
+export const LifecycleEffectMask = /* */ 0b00001110100100;
33
34
// Union of all host effects
34
-export const HostEffectMask = /* */ 0b0011111111111;
35
+export const HostEffectMask = /* */ 0b00011111111111;
36
36
-export const Incomplete = /* */ 0b0100000000000;
37
-export const ShouldCapture = /* */ 0b1000000000000;
37
+export const Incomplete = /* */ 0b00100000000000;
38
+export const ShouldCapture = /* */ 0b01000000000000;
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+89
@@ -1252,6 +1252,95 @@ describe('ReactHooksWithNoopRenderer', () => {
1252
});
1253
});
1254
1255
+ // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
1256
+ it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', () => {
1257
+ let setParentState = null;
1258
+ const setChildStates = [];
1259
+
1260
+ function Parent() {
1261
+ const [state, setState] = useState(true);
1262
+ setParentState = setState;
1263
+ Scheduler.unstable_yieldValue(`Parent ${state} render`);
1264
+ useLayoutEffect(() => {
1265
+ Scheduler.unstable_yieldValue(`Parent ${state} commit`);
1266
+ });
1267
+ if (state) {
1268
+ return (
1269
+ <>
1270
+ <Child label="one" />
1271
+ <Child label="two" />
1272
+ </>
1273
+ );
1274
+ } else {
1275
+ return null;
1276
+ }
1277
+ }
1278
+
1279
+ function Child({label}) {
1280
+ const [state, setState] = useState(0);
1281
+ useLayoutEffect(() => {
1282
+ Scheduler.unstable_yieldValue(`Child ${label} commit`);
1283
+ });
1284
+ useEffect(() => {
1285
+ setChildStates.push(setState);
1286
+ Scheduler.unstable_yieldValue(`Child ${label} passive create`);
1287
+ return () => {
1288
+ Scheduler.unstable_yieldValue(`Child ${label} passive destroy`);
1289
+ };
1290
+ }, []);
1291
+ Scheduler.unstable_yieldValue(`Child ${label} render`);
1292
+ return state;
1293
+ }
1294
+
1295
+ // Schedule debounced state update for child (prob a no-op for this test)
1296
+ // later tick: schedule unmount for parent
1297
+ // start process unmount (but don't flush passive effectS)
1298
+ // State update on child
1299
+ act(() => {
1300
+ ReactNoop.render(<Parent />);
1301
+ expect(Scheduler).toFlushAndYieldThrough([
1302
+ 'Parent true render',
1303
+ 'Child one render',
1304
+ 'Child two render',
1305
+ 'Child one commit',
1306
+ 'Child two commit',
1307
+ 'Parent true commit',
1308
+ 'Child one passive create',
1309
+ 'Child two passive create',
1310
+ ]);
1311
+
1312
+ // Update children.
1313
+ setChildStates.forEach(setChildState => setChildState(1));
1314
+ expect(Scheduler).toFlushAndYieldThrough([
1315
+ 'Child one render',
1316
+ 'Child two render',
1317
+ 'Child one commit',
1318
+ 'Child two commit',
1319
+ ]);
1320
+
1321
+ // Schedule another update for children, and partially process it.
1322
+ setChildStates.forEach(setChildState => setChildState(2));
1323
+ expect(Scheduler).toFlushAndYieldThrough(['Child one render']);
1324
+
1325
+ // Schedule unmount for the parent that unmounts children with pending update.
1326
+ Scheduler.unstable_runWithPriority(
1327
+ Scheduler.unstable_UserBlockingPriority,
1328
+ () => setParentState(false),
1329
+ );
1330
+ expect(Scheduler).toFlushAndYieldThrough([
1331
+ 'Parent false render',
1332
+ 'Parent false commit',
1333
+ ]);
1334
+
1335
+ // Schedule updates for children too (which should be ignored)
1336
+ setChildStates.forEach(setChildState => setChildState(2));
1337
+ expect(Scheduler).toFlushAndYield([
1338
+ 'Child one passive destroy',
1339
+ 'Child two passive destroy',
1340
+ ]);
1341
+ });
1342
+ });
1343
+
1344
it('warns about state updates for unmounted components with no pending passive unmounts', () => {
1345
let completePendingRequest = null;
1346
function Component() {