@samitouri / QOS-React-2 / commits / 263bc5d54b

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() {