Re-add "strict effects mode" for legacy roots only (#20639)
This combines changes originally made in #19523, #20028, and #20415 but with slightly different semantics: "strict effects" mode is enabled only for the experimental root APIs (never for legacy render, regardless of <StrictMode> usage). These semantics may change slightly in the future.
Brian Vaughn committed
Jan 22, 2021 at 05:58 UTC
3957853ae5095a85bacf6d414ee200e8236e9ed4
10 files changed
+707
-102
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+49
-6
@@ -12,13 +12,14 @@ import type {Lanes} from './ReactFiberLane.new';
12
import type {UpdateQueue} from './ReactUpdateQueue.new';
13
14
import * as React from 'react';
15
-import {Update, Snapshot} from './ReactFiberFlags';
15
+import {MountLayoutDev, Update, Snapshot} from './ReactFiberFlags';
16
import {
17
debugRenderPhaseSideEffectsForStrictMode,
18
disableLegacyContext,
19
enableDebugTracing,
20
enableSchedulingProfiler,
21
warnAboutDeprecatedLifecycles,
22
+ enableDoubleInvokingEffects,
23
} from 'shared/ReactFeatureFlags';
24
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
25
import {isMounted} from './ReactFiberTreeReflection';
@@ -29,7 +30,13 @@ import invariant from 'shared/invariant';
30
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
31
32
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
32
-import {DebugTracingMode, StrictMode} from './ReactTypeOfMode';
33
+import {
34
+ BlockingMode,
35
+ ConcurrentMode,
36
+ DebugTracingMode,
37
+ NoMode,
38
+ StrictMode,
39
+} from './ReactTypeOfMode';
40
41
import {
42
enqueueUpdate,
@@ -890,7 +897,16 @@ function mountClassInstance(
897
}
898
899
if (typeof instance.componentDidMount === 'function') {
893
- workInProgress.flags |= Update;
900
+ if (
901
+ __DEV__ &&
902
+ enableDoubleInvokingEffects &&
903
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
904
+ ) {
905
+ // Never double-invoke effects for legacy roots.
906
+ workInProgress.flags |= MountLayoutDev | Update;
907
+ } else {
908
+ workInProgress.flags |= Update;
909
+ }
910
}
911
}
912
@@ -960,7 +976,16 @@ function resumeMountClassInstance(
976
// If an update was already in progress, we should schedule an Update
977
// effect even though we're bailing out, so that cWU/cDU are called.
978
if (typeof instance.componentDidMount === 'function') {
963
- workInProgress.flags |= Update;
979
+ if (
980
+ __DEV__ &&
981
+ enableDoubleInvokingEffects &&
982
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
983
+ ) {
984
+ // Never double-invoke effects for legacy roots.
985
+ workInProgress.flags |= MountLayoutDev | Update;
986
+ } else {
987
+ workInProgress.flags |= Update;
988
+ }
989
}
990
return false;
991
}
@@ -1003,13 +1028,31 @@ function resumeMountClassInstance(
1028
}
1029
}
1030
if (typeof instance.componentDidMount === 'function') {
1006
- workInProgress.flags |= Update;
1031
+ if (
1032
+ __DEV__ &&
1033
+ enableDoubleInvokingEffects &&
1034
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1035
+ ) {
1036
+ // Never double-invoke effects for legacy roots.
1037
+ workInProgress.flags |= MountLayoutDev | Update;
1038
+ } else {
1039
+ workInProgress.flags |= Update;
1040
+ }
1041
}
1042
} else {
1043
// If an update was already in progress, we should schedule an Update
1044
// effect even though we're bailing out, so that cWU/cDU are called.
1045
if (typeof instance.componentDidMount === 'function') {
1012
- workInProgress.flags |= Update;
1046
+ if (
1047
+ __DEV__ &&
1048
+ enableDoubleInvokingEffects &&
1049
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1050
+ ) {
1051
+ // Never double-invoke effects for legacy roots.
1052
+ workInProgress.flags |= MountLayoutDev | Update;
1053
+ } else {
1054
+ workInProgress.flags |= Update;
1055
+ }
1056
}
1057
1058
// If shouldComponentUpdate returned false, we should still update the
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+49
-6
@@ -12,13 +12,14 @@ import type {Lanes} from './ReactFiberLane.old';
12
import type {UpdateQueue} from './ReactUpdateQueue.old';
13
14
import * as React from 'react';
15
-import {Update, Snapshot} from './ReactFiberFlags';
15
+import {MountLayoutDev, Update, Snapshot} from './ReactFiberFlags';
16
import {
17
debugRenderPhaseSideEffectsForStrictMode,
18
disableLegacyContext,
19
enableDebugTracing,
20
enableSchedulingProfiler,
21
warnAboutDeprecatedLifecycles,
22
+ enableDoubleInvokingEffects,
23
} from 'shared/ReactFeatureFlags';
24
import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
25
import {isMounted} from './ReactFiberTreeReflection';
@@ -29,7 +30,13 @@ import invariant from 'shared/invariant';
30
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
31
32
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
32
-import {DebugTracingMode, StrictMode} from './ReactTypeOfMode';
33
+import {
34
+ BlockingMode,
35
+ ConcurrentMode,
36
+ DebugTracingMode,
37
+ NoMode,
38
+ StrictMode,
39
+} from './ReactTypeOfMode';
40
41
import {
42
enqueueUpdate,
@@ -890,7 +897,16 @@ function mountClassInstance(
897
}
898
899
if (typeof instance.componentDidMount === 'function') {
893
- workInProgress.flags |= Update;
900
+ if (
901
+ __DEV__ &&
902
+ enableDoubleInvokingEffects &&
903
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
904
+ ) {
905
+ // Never double-invoke effects for legacy roots.
906
+ workInProgress.flags |= MountLayoutDev | Update;
907
+ } else {
908
+ workInProgress.flags |= Update;
909
+ }
910
}
911
}
912
@@ -960,7 +976,16 @@ function resumeMountClassInstance(
976
// If an update was already in progress, we should schedule an Update
977
// effect even though we're bailing out, so that cWU/cDU are called.
978
if (typeof instance.componentDidMount === 'function') {
963
- workInProgress.flags |= Update;
979
+ if (
980
+ __DEV__ &&
981
+ enableDoubleInvokingEffects &&
982
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
983
+ ) {
984
+ // Never double-invoke effects for legacy roots.
985
+ workInProgress.flags |= MountLayoutDev | Update;
986
+ } else {
987
+ workInProgress.flags |= Update;
988
+ }
989
}
990
return false;
991
}
@@ -1003,13 +1028,31 @@ function resumeMountClassInstance(
1028
}
1029
}
1030
if (typeof instance.componentDidMount === 'function') {
1006
- workInProgress.flags |= Update;
1031
+ if (
1032
+ __DEV__ &&
1033
+ enableDoubleInvokingEffects &&
1034
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1035
+ ) {
1036
+ // Never double-invoke effects for legacy roots.
1037
+ workInProgress.flags |= MountLayoutDev | Update;
1038
+ } else {
1039
+ workInProgress.flags |= Update;
1040
+ }
1041
}
1042
} else {
1043
// If an update was already in progress, we should schedule an Update
1044
// effect even though we're bailing out, so that cWU/cDU are called.
1045
if (typeof instance.componentDidMount === 'function') {
1012
- workInProgress.flags |= Update;
1046
+ if (
1047
+ __DEV__ &&
1048
+ enableDoubleInvokingEffects &&
1049
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1050
+ ) {
1051
+ // Never double-invoke effects for legacy roots.
1052
+ workInProgress.flags |= MountLayoutDev | Update;
1053
+ } else {
1054
+ workInProgress.flags |= Update;
1055
+ }
1056
}
1057
1058
// If shouldComponentUpdate returned false, we should still update the
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+134
@@ -36,6 +36,7 @@ import {
36
enableFundamentalAPI,
37
enableSuspenseCallback,
38
enableScopeAPI,
39
+ enableDoubleInvokingEffects,
40
} from 'shared/ReactFeatureFlags';
41
import {
42
FunctionComponent,
@@ -2436,6 +2437,135 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) {
2437
fiber.return = expectedReturnFiber;
2438
}
2439
2440
+function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
2441
+ if (__DEV__ && enableDoubleInvokingEffects) {
2442
+ // We don't need to re-check for legacy roots here.
2443
+ // This function will not be called within legacy roots.
2444
+ switch (fiber.tag) {
2445
+ case FunctionComponent:
2446
+ case ForwardRef:
2447
+ case SimpleMemoComponent: {
2448
+ invokeGuardedCallback(
2449
+ null,
2450
+ commitHookEffectListMount,
2451
+ null,
2452
+ HookLayout | HookHasEffect,
2453
+ fiber,
2454
+ );
2455
+ if (hasCaughtError()) {
2456
+ const mountError = clearCaughtError();
2457
+ captureCommitPhaseError(fiber, mountError);
2458
+ }
2459
+ break;
2460
+ }
2461
+ case ClassComponent: {
2462
+ const instance = fiber.stateNode;
2463
+ invokeGuardedCallback(null, instance.componentDidMount, instance);
2464
+ if (hasCaughtError()) {
2465
+ const mountError = clearCaughtError();
2466
+ captureCommitPhaseError(fiber, mountError);
2467
+ }
2468
+ break;
2469
+ }
2470
+ }
2471
+ }
2472
+}
2473
+
2474
+function invokePassiveEffectMountInDEV(fiber: Fiber): void {
2475
+ if (__DEV__ && enableDoubleInvokingEffects) {
2476
+ // We don't need to re-check for legacy roots here.
2477
+ // This function will not be called within legacy roots.
2478
+ switch (fiber.tag) {
2479
+ case FunctionComponent:
2480
+ case ForwardRef:
2481
+ case SimpleMemoComponent: {
2482
+ invokeGuardedCallback(
2483
+ null,
2484
+ commitHookEffectListMount,
2485
+ null,
2486
+ HookPassive | HookHasEffect,
2487
+ fiber,
2488
+ );
2489
+ if (hasCaughtError()) {
2490
+ const mountError = clearCaughtError();
2491
+ captureCommitPhaseError(fiber, mountError);
2492
+ }
2493
+ break;
2494
+ }
2495
+ }
2496
+ }
2497
+}
2498
+
2499
+function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
2500
+ if (__DEV__ && enableDoubleInvokingEffects) {
2501
+ // We don't need to re-check for legacy roots here.
2502
+ // This function will not be called within legacy roots.
2503
+ switch (fiber.tag) {
2504
+ case FunctionComponent:
2505
+ case ForwardRef:
2506
+ case SimpleMemoComponent: {
2507
+ invokeGuardedCallback(
2508
+ null,
2509
+ commitHookEffectListUnmount,
2510
+ null,
2511
+ HookLayout | HookHasEffect,
2512
+ fiber,
2513
+ fiber.return,
2514
+ );
2515
+ if (hasCaughtError()) {
2516
+ const unmountError = clearCaughtError();
2517
+ captureCommitPhaseError(fiber, unmountError);
2518
+ }
2519
+ break;
2520
+ }
2521
+ case ClassComponent: {
2522
+ const instance = fiber.stateNode;
2523
+ if (typeof instance.componentWillUnmount === 'function') {
2524
+ invokeGuardedCallback(
2525
+ null,
2526
+ safelyCallComponentWillUnmount,
2527
+ null,
2528
+ fiber,
2529
+ instance,
2530
+ fiber.return,
2531
+ );
2532
+ if (hasCaughtError()) {
2533
+ const unmountError = clearCaughtError();
2534
+ captureCommitPhaseError(fiber, unmountError);
2535
+ }
2536
+ }
2537
+ break;
2538
+ }
2539
+ }
2540
+ }
2541
+}
2542
+
2543
+function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
2544
+ if (__DEV__ && enableDoubleInvokingEffects) {
2545
+ // We don't need to re-check for legacy roots here.
2546
+ // This function will not be called within legacy roots.
2547
+ switch (fiber.tag) {
2548
+ case FunctionComponent:
2549
+ case ForwardRef:
2550
+ case SimpleMemoComponent: {
2551
+ invokeGuardedCallback(
2552
+ null,
2553
+ commitHookEffectListUnmount,
2554
+ null,
2555
+ HookPassive | HookHasEffect,
2556
+ fiber,
2557
+ fiber.return,
2558
+ );
2559
+ if (hasCaughtError()) {
2560
+ const unmountError = clearCaughtError();
2561
+ captureCommitPhaseError(fiber, unmountError);
2562
+ }
2563
+ break;
2564
+ }
2565
+ }
2566
+ }
2567
+}
2568
+
2569
export {
2570
commitResetTextContent,
2571
commitPlacement,
@@ -2443,4 +2573,8 @@ export {
2573
commitWork,
2574
commitAttachRef,
2575
commitDetachRef,
2576
+ invokeLayoutEffectMountInDEV,
2577
+ invokeLayoutEffectUnmountInDEV,
2578
+ invokePassiveEffectMountInDEV,
2579
+ invokePassiveEffectUnmountInDEV,
2580
};
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+134
@@ -36,6 +36,7 @@ import {
36
enableFundamentalAPI,
37
enableSuspenseCallback,
38
enableScopeAPI,
39
+ enableDoubleInvokingEffects,
40
} from 'shared/ReactFeatureFlags';
41
import {
42
FunctionComponent,
@@ -2352,6 +2353,135 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) {
2353
fiber.return = expectedReturnFiber;
2354
}
2355
2356
+function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
2357
+ if (__DEV__ && enableDoubleInvokingEffects) {
2358
+ // We don't need to re-check for legacy roots here.
2359
+ // This function will not be called within legacy roots.
2360
+ switch (fiber.tag) {
2361
+ case FunctionComponent:
2362
+ case ForwardRef:
2363
+ case SimpleMemoComponent: {
2364
+ invokeGuardedCallback(
2365
+ null,
2366
+ commitHookEffectListMount,
2367
+ null,
2368
+ HookLayout | HookHasEffect,
2369
+ fiber,
2370
+ );
2371
+ if (hasCaughtError()) {
2372
+ const mountError = clearCaughtError();
2373
+ captureCommitPhaseError(fiber, mountError);
2374
+ }
2375
+ break;
2376
+ }
2377
+ case ClassComponent: {
2378
+ const instance = fiber.stateNode;
2379
+ invokeGuardedCallback(null, instance.componentDidMount, instance);
2380
+ if (hasCaughtError()) {
2381
+ const mountError = clearCaughtError();
2382
+ captureCommitPhaseError(fiber, mountError);
2383
+ }
2384
+ break;
2385
+ }
2386
+ }
2387
+ }
2388
+}
2389
+
2390
+function invokePassiveEffectMountInDEV(fiber: Fiber): void {
2391
+ if (__DEV__ && enableDoubleInvokingEffects) {
2392
+ // We don't need to re-check for legacy roots here.
2393
+ // This function will not be called within legacy roots.
2394
+ switch (fiber.tag) {
2395
+ case FunctionComponent:
2396
+ case ForwardRef:
2397
+ case SimpleMemoComponent: {
2398
+ invokeGuardedCallback(
2399
+ null,
2400
+ commitHookEffectListMount,
2401
+ null,
2402
+ HookPassive | HookHasEffect,
2403
+ fiber,
2404
+ );
2405
+ if (hasCaughtError()) {
2406
+ const mountError = clearCaughtError();
2407
+ captureCommitPhaseError(fiber, mountError);
2408
+ }
2409
+ break;
2410
+ }
2411
+ }
2412
+ }
2413
+}
2414
+
2415
+function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
2416
+ if (__DEV__ && enableDoubleInvokingEffects) {
2417
+ // We don't need to re-check for legacy roots here.
2418
+ // This function will not be called within legacy roots.
2419
+ switch (fiber.tag) {
2420
+ case FunctionComponent:
2421
+ case ForwardRef:
2422
+ case SimpleMemoComponent: {
2423
+ invokeGuardedCallback(
2424
+ null,
2425
+ commitHookEffectListUnmount,
2426
+ null,
2427
+ HookLayout | HookHasEffect,
2428
+ fiber,
2429
+ fiber.return,
2430
+ );
2431
+ if (hasCaughtError()) {
2432
+ const unmountError = clearCaughtError();
2433
+ captureCommitPhaseError(fiber, unmountError);
2434
+ }
2435
+ break;
2436
+ }
2437
+ case ClassComponent: {
2438
+ const instance = fiber.stateNode;
2439
+ if (typeof instance.componentWillUnmount === 'function') {
2440
+ invokeGuardedCallback(
2441
+ null,
2442
+ safelyCallComponentWillUnmount,
2443
+ null,
2444
+ fiber,
2445
+ instance,
2446
+ fiber.return,
2447
+ );
2448
+ if (hasCaughtError()) {
2449
+ const unmountError = clearCaughtError();
2450
+ captureCommitPhaseError(fiber, unmountError);
2451
+ }
2452
+ }
2453
+ break;
2454
+ }
2455
+ }
2456
+ }
2457
+}
2458
+
2459
+function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
2460
+ if (__DEV__ && enableDoubleInvokingEffects) {
2461
+ // We don't need to re-check for legacy roots here.
2462
+ // This function will not be called within legacy roots.
2463
+ switch (fiber.tag) {
2464
+ case FunctionComponent:
2465
+ case ForwardRef:
2466
+ case SimpleMemoComponent: {
2467
+ invokeGuardedCallback(
2468
+ null,
2469
+ commitHookEffectListUnmount,
2470
+ null,
2471
+ HookPassive | HookHasEffect,
2472
+ fiber,
2473
+ fiber.return,
2474
+ );
2475
+ if (hasCaughtError()) {
2476
+ const unmountError = clearCaughtError();
2477
+ captureCommitPhaseError(fiber, unmountError);
2478
+ }
2479
+ break;
2480
+ }
2481
+ }
2482
+ }
2483
+}
2484
+
2485
export {
2486
commitBeforeMutationLifeCycles,
2487
commitResetTextContent,
@@ -2360,4 +2490,8 @@ export {
2490
commitWork,
2491
commitAttachRef,
2492
commitDetachRef,
2493
+ invokeLayoutEffectMountInDEV,
2494
+ invokeLayoutEffectUnmountInDEV,
2495
+ invokePassiveEffectMountInDEV,
2496
+ invokePassiveEffectUnmountInDEV,
2497
};
packages/react-reconciler/src/ReactFiberHooks.new.js
+80
-16
@@ -29,9 +29,15 @@ import {
29
enableCache,
30
decoupleUpdatePriorityFromScheduler,
31
enableUseRefAccessWarning,
32
+ enableDoubleInvokingEffects,
33
} from 'shared/ReactFeatureFlags';
34
34
-import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
35
+import {
36
+ NoMode,
37
+ BlockingMode,
38
+ ConcurrentMode,
39
+ DebugTracingMode,
40
+} from './ReactTypeOfMode';
41
import {
42
NoLane,
43
NoLanes,
@@ -52,6 +58,8 @@ import {
58
Update as UpdateEffect,
59
Passive as PassiveEffect,
60
PassiveStatic as PassiveStaticEffect,
61
+ MountLayoutDev as MountLayoutDevEffect,
62
+ MountPassiveDev as MountPassiveDevEffect,
63
} from './ReactFiberFlags';
64
import {
65
HasEffect as HookHasEffect,
@@ -474,7 +482,20 @@ export function bailoutHooks(
482
workInProgress.updateQueue = current.updateQueue;
483
// TODO: Don't need to reset the flags here, because they're reset in the
484
// complete phase (bubbleProperties).
477
- workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
485
+ if (
486
+ __DEV__ &&
487
+ enableDoubleInvokingEffects &&
488
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
489
+ ) {
490
+ workInProgress.flags &= ~(
491
+ MountPassiveDevEffect |
492
+ MountLayoutDevEffect |
493
+ PassiveEffect |
494
+ UpdateEffect
495
+ );
496
+ } else {
497
+ workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
498
+ }
499
current.lanes = removeLanes(current.lanes, lanes);
500
}
501
@@ -1310,12 +1331,25 @@ function mountEffect(
1331
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1332
}
1333
}
1313
- return mountEffectImpl(
1314
- PassiveEffect | PassiveStaticEffect,
1315
- HookPassive,
1316
- create,
1317
- deps,
1318
- );
1334
+ if (
1335
+ __DEV__ &&
1336
+ enableDoubleInvokingEffects &&
1337
+ (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1338
+ ) {
1339
+ return mountEffectImpl(
1340
+ MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
1341
+ HookPassive,
1342
+ create,
1343
+ deps,
1344
+ );
1345
+ } else {
1346
+ return mountEffectImpl(
1347
+ PassiveEffect | PassiveStaticEffect,
1348
+ HookPassive,
1349
+ create,
1350
+ deps,
1351
+ );
1352
+ }
1353
}
1354
1355
function updateEffect(
@@ -1335,7 +1369,20 @@ function mountLayoutEffect(
1369
create: () => (() => void) | void,
1370
deps: Array<mixed> | void | null,
1371
): void {
1338
- return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1372
+ if (
1373
+ __DEV__ &&
1374
+ enableDoubleInvokingEffects &&
1375
+ (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1376
+ ) {
1377
+ return mountEffectImpl(
1378
+ MountLayoutDevEffect | UpdateEffect,
1379
+ HookLayout,
1380
+ create,
1381
+ deps,
1382
+ );
1383
+ } else {
1384
+ return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1385
+ }
1386
}
1387
1388
function updateLayoutEffect(
@@ -1394,12 +1441,25 @@ function mountImperativeHandle<T>(
1441
const effectDeps =
1442
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
1443
1397
- return mountEffectImpl(
1398
- UpdateEffect,
1399
- HookLayout,
1400
- imperativeHandleEffect.bind(null, create, ref),
1401
- effectDeps,
1402
- );
1444
+ if (
1445
+ __DEV__ &&
1446
+ enableDoubleInvokingEffects &&
1447
+ (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1448
+ ) {
1449
+ return mountEffectImpl(
1450
+ MountLayoutDevEffect | UpdateEffect,
1451
+ HookLayout,
1452
+ imperativeHandleEffect.bind(null, create, ref),
1453
+ effectDeps,
1454
+ );
1455
+ } else {
1456
+ return mountEffectImpl(
1457
+ UpdateEffect,
1458
+ HookLayout,
1459
+ imperativeHandleEffect.bind(null, create, ref),
1460
+ effectDeps,
1461
+ );
1462
+ }
1463
}
1464
1465
function updateImperativeHandle<T>(
@@ -1680,7 +1740,11 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
1740
const setId = mountState(id)[1];
1741
1742
if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1683
- currentlyRenderingFiber.flags |= PassiveEffect;
1743
+ if (__DEV__ && enableDoubleInvokingEffects) {
1744
+ currentlyRenderingFiber.flags |= MountPassiveDevEffect | PassiveEffect;
1745
+ } else {
1746
+ currentlyRenderingFiber.flags |= PassiveEffect;
1747
+ }
1748
pushEffect(
1749
HookHasEffect | HookPassive,
1750
() => {
packages/react-reconciler/src/ReactFiberHooks.old.js
+86
-16
@@ -29,9 +29,15 @@ import {
29
enableCache,
30
decoupleUpdatePriorityFromScheduler,
31
enableUseRefAccessWarning,
32
+ enableDoubleInvokingEffects,
33
} from 'shared/ReactFeatureFlags';
34
34
-import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
35
+import {
36
+ NoMode,
37
+ BlockingMode,
38
+ ConcurrentMode,
39
+ DebugTracingMode,
40
+} from './ReactTypeOfMode';
41
import {
42
NoLane,
43
NoLanes,
@@ -52,6 +58,8 @@ import {
58
Update as UpdateEffect,
59
Passive as PassiveEffect,
60
PassiveStatic as PassiveStaticEffect,
61
+ MountLayoutDev as MountLayoutDevEffect,
62
+ MountPassiveDev as MountPassiveDevEffect,
63
} from './ReactFiberFlags';
64
import {
65
HasEffect as HookHasEffect,
@@ -472,7 +480,22 @@ export function bailoutHooks(
480
lanes: Lanes,
481
) {
482
workInProgress.updateQueue = current.updateQueue;
475
- workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
483
+ // TODO: Don't need to reset the flags here, because they're reset in the
484
+ // complete phase (bubbleProperties).
485
+ if (
486
+ __DEV__ &&
487
+ enableDoubleInvokingEffects &&
488
+ (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode
489
+ ) {
490
+ workInProgress.flags &= ~(
491
+ MountLayoutDevEffect |
492
+ MountPassiveDevEffect |
493
+ PassiveEffect |
494
+ UpdateEffect
495
+ );
496
+ } else {
497
+ workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
498
+ }
499
current.lanes = removeLanes(current.lanes, lanes);
500
}
501
@@ -1308,12 +1331,28 @@ function mountEffect(
1331
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1332
}
1333
}
1311
- return mountEffectImpl(
1312
- UpdateEffect | PassiveEffect | PassiveStaticEffect,
1313
- HookPassive,
1314
- create,
1315
- deps,
1316
- );
1334
+ if (
1335
+ __DEV__ &&
1336
+ enableDoubleInvokingEffects &&
1337
+ (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1338
+ ) {
1339
+ return mountEffectImpl(
1340
+ MountPassiveDevEffect |
1341
+ UpdateEffect |
1342
+ PassiveEffect |
1343
+ PassiveStaticEffect,
1344
+ HookPassive,
1345
+ create,
1346
+ deps,
1347
+ );
1348
+ } else {
1349
+ return mountEffectImpl(
1350
+ UpdateEffect | PassiveEffect | PassiveStaticEffect,
1351
+ HookPassive,
1352
+ create,
1353
+ deps,
1354
+ );
1355
+ }
1356
}
1357
1358
function updateEffect(
@@ -1338,7 +1377,20 @@ function mountLayoutEffect(
1377
create: () => (() => void) | void,
1378
deps: Array<mixed> | void | null,
1379
): void {
1341
- return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1380
+ if (
1381
+ __DEV__ &&
1382
+ enableDoubleInvokingEffects &&
1383
+ (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1384
+ ) {
1385
+ return mountEffectImpl(
1386
+ MountLayoutDevEffect | UpdateEffect,
1387
+ HookLayout,
1388
+ create,
1389
+ deps,
1390
+ );
1391
+ } else {
1392
+ return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1393
+ }
1394
}
1395
1396
function updateLayoutEffect(
@@ -1397,12 +1449,25 @@ function mountImperativeHandle<T>(
1449
const effectDeps =
1450
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
1451
1400
- return mountEffectImpl(
1401
- UpdateEffect,
1402
- HookLayout,
1403
- imperativeHandleEffect.bind(null, create, ref),
1404
- effectDeps,
1405
- );
1452
+ if (
1453
+ __DEV__ &&
1454
+ enableDoubleInvokingEffects &&
1455
+ (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode
1456
+ ) {
1457
+ return mountEffectImpl(
1458
+ MountLayoutDevEffect | UpdateEffect,
1459
+ HookLayout,
1460
+ imperativeHandleEffect.bind(null, create, ref),
1461
+ effectDeps,
1462
+ );
1463
+ } else {
1464
+ return mountEffectImpl(
1465
+ UpdateEffect,
1466
+ HookLayout,
1467
+ imperativeHandleEffect.bind(null, create, ref),
1468
+ effectDeps,
1469
+ );
1470
+ }
1471
}
1472
1473
function updateImperativeHandle<T>(
@@ -1683,7 +1748,12 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
1748
const setId = mountState(id)[1];
1749
1750
if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1686
- currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
1751
+ if (__DEV__ && enableDoubleInvokingEffects) {
1752
+ currentlyRenderingFiber.flags |=
1753
+ MountPassiveDevEffect | UpdateEffect | PassiveEffect;
1754
+ } else {
1755
+ currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
1756
+ }
1757
pushEffect(
1758
HookHasEffect | HookPassive,
1759
() => {
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+72
@@ -15,6 +15,7 @@ import type {Interaction} from 'scheduler/src/Tracing';
15
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
16
import type {StackCursor} from './ReactFiberStack.new';
17
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
18
+import type {Flags} from './ReactFiberFlags';
19
20
import {
21
warnAboutDeprecatedLifecycles,
@@ -31,6 +32,7 @@ import {
32
enableDebugTracing,
33
enableSchedulingProfiler,
34
disableSchedulerTimeoutInWorkLoop,
35
+ enableDoubleInvokingEffects,
36
} from 'shared/ReactFeatureFlags';
37
import ReactSharedInternals from 'shared/ReactSharedInternals';
38
import invariant from 'shared/invariant';
@@ -126,6 +128,8 @@ import {
128
MutationMask,
129
LayoutMask,
130
PassiveMask,
131
+ MountPassiveDev,
132
+ MountLayoutDev,
133
} from './ReactFiberFlags';
134
import {
135
NoLanePriority,
@@ -182,6 +186,10 @@ import {
186
commitPassiveEffectDurations,
187
commitPassiveMountEffects,
188
commitPassiveUnmountEffects,
189
+ invokeLayoutEffectMountInDEV,
190
+ invokePassiveEffectMountInDEV,
191
+ invokeLayoutEffectUnmountInDEV,
192
+ invokePassiveEffectUnmountInDEV,
193
} from './ReactFiberCommitWork.new';
194
import {enqueueUpdate} from './ReactUpdateQueue.new';
195
import {resetContextDependencies} from './ReactFiberNewContext.new';
@@ -2041,6 +2049,12 @@ function commitRootImpl(root, renderPriorityLevel) {
2049
legacyErrorBoundariesThatAlreadyFailed = null;
2050
}
2051
2052
+ if (__DEV__ && enableDoubleInvokingEffects) {
2053
+ if (!rootDidHavePassiveEffects) {
2054
+ commitDoubleInvokeEffectsInDEV(root.current, false);
2055
+ }
2056
+ }
2057
+
2058
if (enableSchedulerTracing) {
2059
if (!rootDidHavePassiveEffects) {
2060
// If there are no passive effects, then we can complete the pending interactions.
@@ -2222,6 +2236,10 @@ function flushPassiveEffectsImpl() {
2236
markPassiveEffectsStopped();
2237
}
2238
2239
+ if (__DEV__ && enableDoubleInvokingEffects) {
2240
+ commitDoubleInvokeEffectsInDEV(root.current, true);
2241
+ }
2242
+
2243
executionContext = prevExecutionContext;
2244
2245
flushSyncCallbackQueue();
@@ -2505,6 +2523,60 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
2523
}
2524
}
2525
2526
+function commitDoubleInvokeEffectsInDEV(
2527
+ fiber: Fiber,
2528
+ hasPassiveEffects: boolean,
2529
+) {
2530
+ if (__DEV__ && enableDoubleInvokingEffects) {
2531
+ // Never double-invoke effects for legacy roots.
2532
+ if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) {
2533
+ return;
2534
+ }
2535
+
2536
+ setCurrentDebugFiberInDEV(fiber);
2537
+ invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
2538
+ if (hasPassiveEffects) {
2539
+ invokeEffectsInDev(
2540
+ fiber,
2541
+ MountPassiveDev,
2542
+ invokePassiveEffectUnmountInDEV,
2543
+ );
2544
+ }
2545
+
2546
+ invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
2547
+ if (hasPassiveEffects) {
2548
+ invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
2549
+ }
2550
+ resetCurrentDebugFiberInDEV();
2551
+ }
2552
+}
2553
+
2554
+// TODO (strict effects) Rewrite to be iterative
2555
+function invokeEffectsInDev(
2556
+ firstChild: Fiber,
2557
+ fiberFlags: Flags,
2558
+ invokeEffectFn: (fiber: Fiber) => void,
2559
+): void {
2560
+ if (__DEV__ && enableDoubleInvokingEffects) {
2561
+ // We don't need to re-check for legacy roots here.
2562
+ // This function will not be called within legacy roots.
2563
+ let fiber = firstChild;
2564
+ while (fiber !== null) {
2565
+ if (fiber.child !== null) {
2566
+ const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags;
2567
+ if (primarySubtreeFlag !== NoFlags) {
2568
+ invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn);
2569
+ }
2570
+ }
2571
+
2572
+ if ((fiber.flags & fiberFlags) !== NoFlags) {
2573
+ invokeEffectFn(fiber);
2574
+ }
2575
+ fiber = fiber.sibling;
2576
+ }
2577
+ }
2578
+}
2579
+
2580
let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
2581
function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
2582
if (__DEV__) {
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+70
@@ -16,6 +16,7 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
16
import type {Effect as HookEffect} from './ReactFiberHooks.old';
17
import type {StackCursor} from './ReactFiberStack.old';
18
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
19
+import type {Flags} from './ReactFiberFlags';
20
21
import {
22
warnAboutDeprecatedLifecycles,
@@ -32,6 +33,7 @@ import {
33
enableDebugTracing,
34
enableSchedulingProfiler,
35
disableSchedulerTimeoutInWorkLoop,
36
+ enableDoubleInvokingEffects,
37
} from 'shared/ReactFeatureFlags';
38
import ReactSharedInternals from 'shared/ReactSharedInternals';
39
import invariant from 'shared/invariant';
@@ -131,6 +133,8 @@ import {
133
HostEffectMask,
134
Hydrating,
135
StaticMask,
136
+ MountPassiveDev,
137
+ MountLayoutDev,
138
} from './ReactFiberFlags';
139
import {
140
NoLanePriority,
@@ -189,6 +193,10 @@ import {
193
commitPassiveMountEffects,
194
commitPassiveUnmountEffects,
195
detachFiberAfterEffects,
196
+ invokeLayoutEffectMountInDEV,
197
+ invokePassiveEffectMountInDEV,
198
+ invokeLayoutEffectUnmountInDEV,
199
+ invokePassiveEffectUnmountInDEV,
200
} from './ReactFiberCommitWork.old';
201
import {enqueueUpdate} from './ReactUpdateQueue.old';
202
import {resetContextDependencies} from './ReactFiberNewContext.old';
@@ -2126,6 +2134,12 @@ function commitRootImpl(root, renderPriorityLevel) {
2134
legacyErrorBoundariesThatAlreadyFailed = null;
2135
}
2136
2137
+ if (__DEV__ && enableDoubleInvokingEffects) {
2138
+ if (!rootDidHavePassiveEffects) {
2139
+ commitDoubleInvokeEffectsInDEV(root.current, false);
2140
+ }
2141
+ }
2142
+
2143
if (enableSchedulerTracing) {
2144
if (!rootDidHavePassiveEffects) {
2145
// If there are no passive effects, then we can complete the pending interactions.
@@ -2379,6 +2393,10 @@ function flushPassiveEffectsImpl() {
2393
markPassiveEffectsStopped();
2394
}
2395
2396
+ if (__DEV__ && enableDoubleInvokingEffects) {
2397
+ commitDoubleInvokeEffectsInDEV(root.current, true);
2398
+ }
2399
+
2400
executionContext = prevExecutionContext;
2401
2402
flushSyncCallbackQueue();
@@ -2662,6 +2680,58 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
2680
}
2681
}
2682
2683
+function commitDoubleInvokeEffectsInDEV(
2684
+ fiber: Fiber,
2685
+ hasPassiveEffects: boolean,
2686
+) {
2687
+ if (__DEV__ && enableDoubleInvokingEffects) {
2688
+ // Never double-invoke effects for legacy roots.
2689
+ if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) {
2690
+ return;
2691
+ }
2692
+
2693
+ setCurrentDebugFiberInDEV(fiber);
2694
+ invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
2695
+ if (hasPassiveEffects) {
2696
+ invokeEffectsInDev(
2697
+ fiber,
2698
+ MountPassiveDev,
2699
+ invokePassiveEffectUnmountInDEV,
2700
+ );
2701
+ }
2702
+
2703
+ invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
2704
+ if (hasPassiveEffects) {
2705
+ invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
2706
+ }
2707
+ resetCurrentDebugFiberInDEV();
2708
+ }
2709
+}
2710
+
2711
+// TODO (strict effects) Rewrite to be iterative
2712
+function invokeEffectsInDev(
2713
+ firstChild: Fiber,
2714
+ fiberFlags: Flags,
2715
+ invokeEffectFn: (fiber: Fiber) => void,
2716
+): void {
2717
+ if (__DEV__ && enableDoubleInvokingEffects) {
2718
+ let fiber = firstChild;
2719
+ while (fiber !== null) {
2720
+ if (fiber.child !== null) {
2721
+ const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags;
2722
+ if (primarySubtreeFlag !== NoFlags) {
2723
+ invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn);
2724
+ }
2725
+ }
2726
+
2727
+ if ((fiber.flags & fiberFlags) !== NoFlags) {
2728
+ invokeEffectFn(fiber);
2729
+ }
2730
+ fiber = fiber.sibling;
2731
+ }
2732
+ }
2733
+}
2734
+
2735
let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
2736
function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
2737
if (__DEV__) {
packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js
+33
-56
@@ -15,8 +15,9 @@ let ReactNoop;
15
let Scheduler;
16
17
function shouldDoubleInvokingEffects() {
18
- // Reverted temporarily. Need to re-land.
19
- return false;
18
+ // For now, this feature only exists in the old fork (while the new fork is being bisected).
19
+ // Eventually we'll land it in both forks.
20
+ return __DEV__;
21
}
22
23
describe('ReactDoubleInvokeEvents', () => {
@@ -30,7 +31,7 @@ describe('ReactDoubleInvokeEvents', () => {
31
ReactFeatureFlags.enableDoubleInvokingEffects = shouldDoubleInvokingEffects();
32
});
33
33
- it('should not double invoke effects outside of StrictMode', () => {
34
+ it('should not double invoke effects in legacy mode', () => {
35
function App({text}) {
36
React.useEffect(() => {
37
Scheduler.unstable_yieldValue('useEffect mount');
@@ -55,54 +56,30 @@ describe('ReactDoubleInvokeEvents', () => {
56
]);
57
});
58
58
- it('should double invoke effects only within StrictMode subtrees', () => {
59
- function ComponentWithEffects({label}) {
60
- React.useEffect(() => {
61
- Scheduler.unstable_yieldValue(`useEffect mount "${label}"`);
62
- return () =>
63
- Scheduler.unstable_yieldValue(`useEffect unmount "${label}"`);
64
- });
59
+ it('should not double invoke class lifecycles in legacy mode', () => {
60
+ class App extends React.PureComponent {
61
+ componentDidMount() {
62
+ Scheduler.unstable_yieldValue('componentDidMount');
63
+ }
64
66
- React.useLayoutEffect(() => {
67
- Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`);
68
- return () =>
69
- Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`);
70
- });
65
+ componentDidUpdate() {
66
+ Scheduler.unstable_yieldValue('componentDidUpdate');
67
+ }
68
72
- return label;
69
+ componentWillUnmount() {
70
+ Scheduler.unstable_yieldValue('componentWillUnmount');
71
+ }
72
+
73
+ render() {
74
+ return this.props.text;
75
+ }
76
}
77
78
ReactNoop.act(() => {
76
- ReactNoop.renderLegacySyncRoot(
77
- <>
78
- <ComponentWithEffects label={'loose'} />
79
- <React.StrictMode>
80
- <ComponentWithEffects label={'strict'} />
81
- </React.StrictMode>
82
- </>,
83
- );
79
+ ReactNoop.renderLegacySyncRoot(<App text={'mount'} />);
80
});
81
86
- if (shouldDoubleInvokingEffects()) {
87
- expect(Scheduler).toHaveYielded([
88
- 'useLayoutEffect mount "loose"',
89
- 'useLayoutEffect mount "strict"',
90
- 'useEffect mount "loose"',
91
- 'useEffect mount "strict"',
92
-
93
- 'useLayoutEffect unmount "strict"',
94
- 'useEffect unmount "strict"',
95
- 'useLayoutEffect mount "strict"',
96
- 'useEffect mount "strict"',
97
- ]);
98
- } else {
99
- expect(Scheduler).toHaveYielded([
100
- 'useLayoutEffect mount "loose"',
101
- 'useLayoutEffect mount "strict"',
102
- 'useEffect mount "loose"',
103
- 'useEffect mount "strict"',
104
- ]);
105
- }
82
+ expect(Scheduler).toHaveYielded(['componentDidMount']);
83
});
84
85
it('should flush double-invoked effects within the same frame as layout effects if there are no passive effects', () => {
@@ -117,10 +94,10 @@ describe('ReactDoubleInvokeEvents', () => {
94
}
95
96
ReactNoop.act(() => {
120
- ReactNoop.renderLegacySyncRoot(
121
- <React.StrictMode>
97
+ ReactNoop.render(
98
+ <>
99
<ComponentWithEffects label={'one'} />
123
- </React.StrictMode>,
100
+ </>,
101
);
102
103
if (shouldDoubleInvokingEffects()) {
@@ -137,11 +114,11 @@ describe('ReactDoubleInvokeEvents', () => {
114
});
115
116
ReactNoop.act(() => {
140
- ReactNoop.renderLegacySyncRoot(
141
- <React.StrictMode>
117
+ ReactNoop.render(
118
+ <>
119
<ComponentWithEffects label={'one'} />
120
<ComponentWithEffects label={'two'} />
144
- </React.StrictMode>,
121
+ </>,
122
);
123
124
if (shouldDoubleInvokingEffects()) {
@@ -185,10 +162,10 @@ describe('ReactDoubleInvokeEvents', () => {
162
}
163
164
ReactNoop.act(() => {
188
- ReactNoop.renderLegacySyncRoot(
189
- <React.StrictMode>
165
+ ReactNoop.render(
166
+ <>
167
<ComponentWithEffects label={'one'} />
191
- </React.StrictMode>,
168
+ </>,
169
);
170
171
if (shouldDoubleInvokingEffects()) {
@@ -211,11 +188,11 @@ describe('ReactDoubleInvokeEvents', () => {
188
});
189
190
ReactNoop.act(() => {
214
- ReactNoop.renderLegacySyncRoot(
215
- <React.StrictMode>
191
+ ReactNoop.render(
192
+ <>
193
<ComponentWithEffects label={'one'} />
194
<ComponentWithEffects label={'two'} />
218
- </React.StrictMode>,
195
+ </>,
196
);
197
198
if (shouldDoubleInvokingEffects()) {
packages/react/src/__tests__/ReactProfiler-test.internal.js
-2
@@ -4874,8 +4874,6 @@ describe('Profiler', () => {
4874
});
4875
4876
if (__DEV__) {
4877
- // @gate enableUseJSStackToTrackPassiveDurations
4878
- // @gate enableDoubleInvokingEffects
4877
it('double invoking does not disconnect wrapped async work', () => {
4878
ReactFeatureFlags.enableDoubleInvokingEffects = true;
4879