@samitouri / QOS-React-2 / commits / 56a632adb6

Double Invoke Effects in __DEV__ (in old reconciler fork) (#20415)

We originally added a new DEV behavior of double-invoking effects during mount to our new reconciler fork in PRs #19523 and #19935 and later refined it to only affect modern roots in PR #20028. This PR adds that behavior to the old reconciler fork with a small twist– the behavior applies to StrictMode subtrees, regardless of the root type. This commit also adds a few additional tests that weren't in the original commits.

Brian Vaughn committed Dec 9, 2020 at 14:18 UTC 56a632adb657fe5b704125caabf29f0cc03b0b12
7 files changed +1099 -50
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+39 -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 {Update, Snapshot, MountLayoutDev} 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,7 @@ 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 {DebugTracingMode, NoMode, StrictMode} from './ReactTypeOfMode';
34
35 import {
36 enqueueUpdate,
@@ -890,7 +891,15 @@ function mountClassInstance(
891 }
892
893 if (typeof instance.componentDidMount === 'function') {
893 - workInProgress.flags |= Update;
894 + if (
895 + __DEV__ &&
896 + enableDoubleInvokingEffects &&
897 + (workInProgress.mode & StrictMode) !== NoMode
898 + ) {
899 + workInProgress.flags |= MountLayoutDev | Update;
900 + } else {
901 + workInProgress.flags |= Update;
902 + }
903 }
904 }
905
@@ -960,7 +969,15 @@ function resumeMountClassInstance(
969 // If an update was already in progress, we should schedule an Update
970 // effect even though we're bailing out, so that cWU/cDU are called.
971 if (typeof instance.componentDidMount === 'function') {
963 - workInProgress.flags |= Update;
972 + if (
973 + __DEV__ &&
974 + enableDoubleInvokingEffects &&
975 + (workInProgress.mode & StrictMode) !== NoMode
976 + ) {
977 + workInProgress.flags |= MountLayoutDev | Update;
978 + } else {
979 + workInProgress.flags |= Update;
980 + }
981 }
982 return false;
983 }
@@ -1003,13 +1020,29 @@ function resumeMountClassInstance(
1020 }
1021 }
1022 if (typeof instance.componentDidMount === 'function') {
1006 - workInProgress.flags |= Update;
1023 + if (
1024 + __DEV__ &&
1025 + enableDoubleInvokingEffects &&
1026 + (workInProgress.mode & StrictMode) !== NoMode
1027 + ) {
1028 + workInProgress.flags |= MountLayoutDev | Update;
1029 + } else {
1030 + workInProgress.flags |= Update;
1031 + }
1032 }
1033 } else {
1034 // If an update was already in progress, we should schedule an Update
1035 // effect even though we're bailing out, so that cWU/cDU are called.
1036 if (typeof instance.componentDidMount === 'function') {
1012 - workInProgress.flags |= Update;
1037 + if (
1038 + __DEV__ &&
1039 + enableDoubleInvokingEffects &&
1040 + (workInProgress.mode & StrictMode) !== NoMode
1041 + ) {
1042 + workInProgress.flags |= MountLayoutDev | Update;
1043 + } else {
1044 + workInProgress.flags |= Update;
1045 + }
1046 }
1047
1048 // If shouldComponentUpdate returned false, we should still update the
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+115
@@ -35,6 +35,7 @@ import {
35 enableFundamentalAPI,
36 enableSuspenseCallback,
37 enableScopeAPI,
38 + enableDoubleInvokingEffects,
39 } from 'shared/ReactFeatureFlags';
40 import {
41 FunctionComponent,
@@ -1798,6 +1799,116 @@ function commitResetTextContent(current: Fiber) {
1799 resetTextContent(current.stateNode);
1800 }
1801
1802 +function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
1803 + if (__DEV__ && enableDoubleInvokingEffects) {
1804 + switch (fiber.tag) {
1805 + case FunctionComponent:
1806 + case ForwardRef:
1807 + case SimpleMemoComponent: {
1808 + invokeGuardedCallback(
1809 + null,
1810 + commitHookEffectListMount,
1811 + null,
1812 + HookLayout | HookHasEffect,
1813 + fiber,
1814 + );
1815 + if (hasCaughtError()) {
1816 + const mountError = clearCaughtError();
1817 + captureCommitPhaseError(fiber, mountError);
1818 + }
1819 + break;
1820 + }
1821 + case ClassComponent: {
1822 + const instance = fiber.stateNode;
1823 + invokeGuardedCallback(null, instance.componentDidMount, instance);
1824 + if (hasCaughtError()) {
1825 + const mountError = clearCaughtError();
1826 + captureCommitPhaseError(fiber, mountError);
1827 + }
1828 + break;
1829 + }
1830 + }
1831 + }
1832 +}
1833 +
1834 +function invokePassiveEffectMountInDEV(fiber: Fiber): void {
1835 + if (__DEV__ && enableDoubleInvokingEffects) {
1836 + switch (fiber.tag) {
1837 + case FunctionComponent:
1838 + case ForwardRef:
1839 + case SimpleMemoComponent: {
1840 + invokeGuardedCallback(
1841 + null,
1842 + commitHookEffectListMount,
1843 + null,
1844 + HookPassive | HookHasEffect,
1845 + fiber,
1846 + );
1847 + if (hasCaughtError()) {
1848 + const mountError = clearCaughtError();
1849 + captureCommitPhaseError(fiber, mountError);
1850 + }
1851 + break;
1852 + }
1853 + }
1854 + }
1855 +}
1856 +
1857 +function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
1858 + if (__DEV__ && enableDoubleInvokingEffects) {
1859 + switch (fiber.tag) {
1860 + case FunctionComponent:
1861 + case ForwardRef:
1862 + case SimpleMemoComponent: {
1863 + invokeGuardedCallback(
1864 + null,
1865 + commitHookEffectListUnmount,
1866 + null,
1867 + HookLayout | HookHasEffect,
1868 + fiber,
1869 + fiber.return,
1870 + );
1871 + if (hasCaughtError()) {
1872 + const unmountError = clearCaughtError();
1873 + captureCommitPhaseError(fiber, unmountError);
1874 + }
1875 + break;
1876 + }
1877 + case ClassComponent: {
1878 + const instance = fiber.stateNode;
1879 + if (typeof instance.componentWillUnmount === 'function') {
1880 + safelyCallComponentWillUnmount(fiber, instance);
1881 + }
1882 + break;
1883 + }
1884 + }
1885 + }
1886 +}
1887 +
1888 +function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
1889 + if (__DEV__ && enableDoubleInvokingEffects) {
1890 + switch (fiber.tag) {
1891 + case FunctionComponent:
1892 + case ForwardRef:
1893 + case SimpleMemoComponent: {
1894 + invokeGuardedCallback(
1895 + null,
1896 + commitHookEffectListUnmount,
1897 + null,
1898 + HookPassive | HookHasEffect,
1899 + fiber,
1900 + fiber.return,
1901 + );
1902 + if (hasCaughtError()) {
1903 + const unmountError = clearCaughtError();
1904 + captureCommitPhaseError(fiber, unmountError);
1905 + }
1906 + break;
1907 + }
1908 + }
1909 + }
1910 +}
1911 +
1912 export {
1913 commitBeforeMutationLifeCycles,
1914 commitResetTextContent,
@@ -1807,4 +1918,8 @@ export {
1918 commitLifeCycles,
1919 commitAttachRef,
1920 commitDetachRef,
1921 + invokeLayoutEffectMountInDEV,
1922 + invokeLayoutEffectUnmountInDEV,
1923 + invokePassiveEffectMountInDEV,
1924 + invokePassiveEffectUnmountInDEV,
1925 };
packages/react-reconciler/src/ReactFiberHooks.old.js
+87 -16
@@ -28,9 +28,15 @@ import {
28 enableCache,
29 decoupleUpdatePriorityFromScheduler,
30 enableUseRefAccessWarning,
31 + enableDoubleInvokingEffects,
32 } from 'shared/ReactFeatureFlags';
33
33 -import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
34 +import {
35 + NoMode,
36 + BlockingMode,
37 + DebugTracingMode,
38 + StrictMode,
39 +} from './ReactTypeOfMode';
40 import {
41 NoLane,
42 NoLanes,
@@ -49,6 +55,8 @@ import {readContext} from './ReactFiberNewContext.old';
55 import {
56 Update as UpdateEffect,
57 Passive as PassiveEffect,
58 + MountLayoutDev as MountLayoutDevEffect,
59 + MountPassiveDev as MountPassiveDevEffect,
60 } from './ReactFiberFlags';
61 import {
62 HasEffect as HookHasEffect,
@@ -467,7 +475,20 @@ export function bailoutHooks(
475 lanes: Lanes,
476 ) {
477 workInProgress.updateQueue = current.updateQueue;
470 - workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
478 + if (
479 + __DEV__ &&
480 + enableDoubleInvokingEffects &&
481 + (workInProgress.mode & StrictMode) !== NoMode
482 + ) {
483 + workInProgress.flags &= ~(
484 + MountLayoutDevEffect |
485 + MountPassiveDevEffect |
486 + PassiveEffect |
487 + UpdateEffect
488 + );
489 + } else {
490 + workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
491 + }
492 current.lanes = removeLanes(current.lanes, lanes);
493 }
494
@@ -1303,12 +1324,26 @@ function mountEffect(
1324 warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1325 }
1326 }
1306 - return mountEffectImpl(
1307 - UpdateEffect | PassiveEffect,
1308 - HookPassive,
1309 - create,
1310 - deps,
1311 - );
1327 +
1328 + if (
1329 + __DEV__ &&
1330 + enableDoubleInvokingEffects &&
1331 + (currentlyRenderingFiber.mode & StrictMode) !== NoMode
1332 + ) {
1333 + return mountEffectImpl(
1334 + UpdateEffect | PassiveEffect | MountPassiveDevEffect,
1335 + HookPassive,
1336 + create,
1337 + deps,
1338 + );
1339 + } else {
1340 + return mountEffectImpl(
1341 + UpdateEffect | PassiveEffect,
1342 + HookPassive,
1343 + create,
1344 + deps,
1345 + );
1346 + }
1347 }
1348
1349 function updateEffect(
@@ -1333,7 +1368,20 @@ function mountLayoutEffect(
1368 create: () => (() => void) | void,
1369 deps: Array<mixed> | void | null,
1370 ): void {
1336 - return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1371 + if (
1372 + __DEV__ &&
1373 + enableDoubleInvokingEffects &&
1374 + (currentlyRenderingFiber.mode & StrictMode) !== NoMode
1375 + ) {
1376 + return mountEffectImpl(
1377 + UpdateEffect | MountLayoutDevEffect,
1378 + HookLayout,
1379 + create,
1380 + deps,
1381 + );
1382 + } else {
1383 + return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1384 + }
1385 }
1386
1387 function updateLayoutEffect(
@@ -1392,12 +1440,25 @@ function mountImperativeHandle<T>(
1440 const effectDeps =
1441 deps !== null && deps !== undefined ? deps.concat([ref]) : null;
1442
1395 - return mountEffectImpl(
1396 - UpdateEffect,
1397 - HookLayout,
1398 - imperativeHandleEffect.bind(null, create, ref),
1399 - effectDeps,
1400 - );
1443 + if (
1444 + __DEV__ &&
1445 + enableDoubleInvokingEffects &&
1446 + (currentlyRenderingFiber.mode & StrictMode) !== NoMode
1447 + ) {
1448 + return mountEffectImpl(
1449 + UpdateEffect | MountLayoutDevEffect,
1450 + HookLayout,
1451 + imperativeHandleEffect.bind(null, create, ref),
1452 + effectDeps,
1453 + );
1454 + } else {
1455 + return mountEffectImpl(
1456 + UpdateEffect,
1457 + HookLayout,
1458 + imperativeHandleEffect.bind(null, create, ref),
1459 + effectDeps,
1460 + );
1461 + }
1462 }
1463
1464 function updateImperativeHandle<T>(
@@ -1678,7 +1739,17 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
1739 const setId = mountState(id)[1];
1740
1741 if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1681 - currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
1742 + if (
1743 + __DEV__ &&
1744 + enableDoubleInvokingEffects &&
1745 + (currentlyRenderingFiber.mode & StrictMode) !== NoMode
1746 + ) {
1747 + currentlyRenderingFiber.flags |=
1748 + UpdateEffect | PassiveEffect | MountPassiveDevEffect;
1749 + } else {
1750 + currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
1751 + }
1752 +
1753 pushEffect(
1754 HookHasEffect | HookPassive,
1755 () => {
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+80 -21
@@ -14,6 +14,7 @@ import type {ReactPriorityLevel} from './ReactInternalTypes';
14 import type {Interaction} from 'scheduler/src/Tracing';
15 import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
16 import type {Effect as HookEffect} from './ReactFiberHooks.old';
17 +import type {Flags} from './ReactFiberFlags';
18 import type {StackCursor} from './ReactFiberStack.old';
19
20 import {
@@ -31,6 +32,7 @@ import {
32 enableDebugTracing,
33 enableSchedulingProfiler,
34 enableScopeAPI,
35 + enableDoubleInvokingEffects,
36 } from 'shared/ReactFeatureFlags';
37 import ReactSharedInternals from 'shared/ReactSharedInternals';
38 import invariant from 'shared/invariant';
@@ -131,6 +133,8 @@ import {
133 HostEffectMask,
134 Hydrating,
135 HydratingAndUpdate,
136 + MountPassiveDev,
137 + MountLayoutDev,
138 } from './ReactFiberFlags';
139 import {
140 NoLanePriority,
@@ -191,6 +195,10 @@ import {
195 commitPassiveEffectDurations,
196 commitResetTextContent,
197 isSuspenseBoundaryBeingHidden,
198 + invokeLayoutEffectMountInDEV,
199 + invokePassiveEffectMountInDEV,
200 + invokeLayoutEffectUnmountInDEV,
201 + invokePassiveEffectUnmountInDEV,
202 } from './ReactFiberCommitWork.old';
203 import {enqueueUpdate} from './ReactUpdateQueue.old';
204 import {resetContextDependencies} from './ReactFiberNewContext.old';
@@ -2113,6 +2121,10 @@ function commitRootImpl(root, renderPriorityLevel) {
2121 pendingPassiveEffectsLanes = lanes;
2122 pendingPassiveEffectsRenderPriority = renderPriorityLevel;
2123 } else {
2124 + if (__DEV__ && enableDoubleInvokingEffects) {
2125 + commitDoubleInvokeEffectsInDEV(root, false);
2126 + }
2127 +
2128 // We are done with the effect chain at this point so let's clear the
2129 // nextEffect pointers to assist with GC. If we have passive effects, we'll
2130 // clear this in flushPassiveEffects.
@@ -2660,6 +2672,29 @@ function flushPassiveEffectsImpl() {
2672 }
2673 }
2674
2675 + if (enableProfilerTimer && enableProfilerCommitHooks) {
2676 + const profilerEffects = pendingPassiveProfilerEffects;
2677 + pendingPassiveProfilerEffects = [];
2678 + for (let i = 0; i < profilerEffects.length; i++) {
2679 + const fiber = ((profilerEffects[i]: any): Fiber);
2680 + commitPassiveEffectDurations(root, fiber);
2681 + }
2682 + }
2683 +
2684 + if (__DEV__) {
2685 + if (enableDebugTracing) {
2686 + logPassiveEffectsStopped();
2687 + }
2688 + }
2689 +
2690 + if (enableSchedulingProfiler) {
2691 + markPassiveEffectsStopped();
2692 + }
2693 +
2694 + if (__DEV__ && enableDoubleInvokingEffects) {
2695 + commitDoubleInvokeEffectsInDEV(root, true);
2696 + }
2697 +
2698 // Note: This currently assumes there are no passive effects on the root fiber
2699 // because the root is not part of its own effect list.
2700 // This could change in the future.
@@ -2674,13 +2709,8 @@ function flushPassiveEffectsImpl() {
2709 effect = nextNextEffect;
2710 }
2711
2677 - if (enableProfilerTimer && enableProfilerCommitHooks) {
2678 - const profilerEffects = pendingPassiveProfilerEffects;
2679 - pendingPassiveProfilerEffects = [];
2680 - for (let i = 0; i < profilerEffects.length; i++) {
2681 - const fiber = ((profilerEffects[i]: any): Fiber);
2682 - commitPassiveEffectDurations(root, fiber);
2683 - }
2712 + if (__DEV__) {
2713 + isFlushingPassiveEffects = false;
2714 }
2715
2716 if (enableSchedulerTracing) {
@@ -2688,20 +2718,6 @@ function flushPassiveEffectsImpl() {
2718 finishPendingInteractions(root, lanes);
2719 }
2720
2691 - if (__DEV__) {
2692 - isFlushingPassiveEffects = false;
2693 - }
2694 -
2695 - if (__DEV__) {
2696 - if (enableDebugTracing) {
2697 - logPassiveEffectsStopped();
2698 - }
2699 - }
2700 -
2701 - if (enableSchedulingProfiler) {
2702 - markPassiveEffectsStopped();
2703 - }
2704 -
2721 executionContext = prevExecutionContext;
2722
2723 flushSyncCallbackQueue();
@@ -2985,6 +3001,49 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
3001 }
3002 }
3003
3004 +function commitDoubleInvokeEffectsInDEV(
3005 + root: FiberRoot,
3006 + hasPassiveEffects: boolean,
3007 +) {
3008 + if (__DEV__ && enableDoubleInvokingEffects) {
3009 + invokeEffectsInDev(root, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
3010 + if (hasPassiveEffects) {
3011 + invokeEffectsInDev(
3012 + root,
3013 + MountPassiveDev,
3014 + invokePassiveEffectUnmountInDEV,
3015 + );
3016 + }
3017 +
3018 + invokeEffectsInDev(root, MountLayoutDev, invokeLayoutEffectMountInDEV);
3019 + if (hasPassiveEffects) {
3020 + invokeEffectsInDev(root, MountPassiveDev, invokePassiveEffectMountInDEV);
3021 + }
3022 + }
3023 +}
3024 +
3025 +function invokeEffectsInDev(
3026 + root: FiberRoot,
3027 + fiberFlags: Flags,
3028 + invokeEffectFn: (fiber: Fiber) => void,
3029 +): void {
3030 + if (__DEV__ && enableDoubleInvokingEffects) {
3031 + // Note: This currently assumes there are no passive effects on the root fiber
3032 + // because the root is not part of its own effect list.
3033 + // This could change in the future.
3034 + let currentEffect = root.current.firstEffect;
3035 + while (currentEffect !== null) {
3036 + const flags = currentEffect.flags;
3037 + if ((flags & fiberFlags) !== NoFlags) {
3038 + setCurrentDebugFiberInDEV(currentEffect);
3039 + invokeEffectFn(currentEffect);
3040 + resetCurrentDebugFiberInDEV();
3041 + }
3042 + currentEffect = currentEffect.nextEffect;
3043 + }
3044 + }
3045 +}
3046 +
3047 let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
3048 function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
3049 if (__DEV__) {
packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js new
+771
@@ -0,0 +1,771 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactNoop;
15 +let Scheduler;
16 +
17 +function shouldDoubleInvokingEffects() {
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__ && !__VARIANT__;
21 +}
22 +
23 +describe('ReactDoubleInvokeEvents', () => {
24 + beforeEach(() => {
25 + jest.resetModules();
26 + React = require('react');
27 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
28 + ReactNoop = require('react-noop-renderer');
29 + Scheduler = require('scheduler');
30 +
31 + ReactFeatureFlags.enableDoubleInvokingEffects = shouldDoubleInvokingEffects();
32 + });
33 +
34 + it('should not double invoke effects outside of StrictMode', () => {
35 + function App({text}) {
36 + React.useEffect(() => {
37 + Scheduler.unstable_yieldValue('useEffect mount');
38 + return () => Scheduler.unstable_yieldValue('useEffect unmount');
39 + });
40 +
41 + React.useLayoutEffect(() => {
42 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
43 + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
44 + });
45 +
46 + return text;
47 + }
48 +
49 + ReactNoop.act(() => {
50 + ReactNoop.renderLegacySyncRoot(<App text={'mount'} />);
51 + });
52 +
53 + expect(Scheduler).toHaveYielded([
54 + 'useLayoutEffect mount',
55 + 'useEffect mount',
56 + ]);
57 + });
58 +
59 + it('should double invoke effects only within StrictMode subtrees', () => {
60 + function ComponentWithEffects({label}) {
61 + React.useEffect(() => {
62 + Scheduler.unstable_yieldValue(`useEffect mount "${label}"`);
63 + return () =>
64 + Scheduler.unstable_yieldValue(`useEffect unmount "${label}"`);
65 + });
66 +
67 + React.useLayoutEffect(() => {
68 + Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`);
69 + return () =>
70 + Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`);
71 + });
72 +
73 + return label;
74 + }
75 +
76 + ReactNoop.act(() => {
77 + ReactNoop.renderLegacySyncRoot(
78 + <>
79 + <ComponentWithEffects label={'loose'} />
80 + <React.StrictMode>
81 + <ComponentWithEffects label={'strict'} />
82 + </React.StrictMode>
83 + </>,
84 + );
85 + });
86 +
87 + if (shouldDoubleInvokingEffects()) {
88 + expect(Scheduler).toHaveYielded([
89 + 'useLayoutEffect mount "loose"',
90 + 'useLayoutEffect mount "strict"',
91 + 'useEffect mount "loose"',
92 + 'useEffect mount "strict"',
93 +
94 + 'useLayoutEffect unmount "strict"',
95 + 'useEffect unmount "strict"',
96 + 'useLayoutEffect mount "strict"',
97 + 'useEffect mount "strict"',
98 + ]);
99 + } else {
100 + expect(Scheduler).toHaveYielded([
101 + 'useLayoutEffect mount "loose"',
102 + 'useLayoutEffect mount "strict"',
103 + 'useEffect mount "loose"',
104 + 'useEffect mount "strict"',
105 + ]);
106 + }
107 + });
108 +
109 + it('should flush double-invoked effects within the same frame as layout effects if there are no passive effects', () => {
110 + function ComponentWithEffects({label}) {
111 + React.useLayoutEffect(() => {
112 + Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`);
113 + return () =>
114 + Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`);
115 + });
116 +
117 + return label;
118 + }
119 +
120 + ReactNoop.act(() => {
121 + ReactNoop.renderLegacySyncRoot(
122 + <React.StrictMode>
123 + <ComponentWithEffects label={'one'} />
124 + </React.StrictMode>,
125 + );
126 +
127 + if (shouldDoubleInvokingEffects()) {
128 + expect(Scheduler).toFlushUntilNextPaint([
129 + 'useLayoutEffect mount "one"',
130 + 'useLayoutEffect unmount "one"',
131 + 'useLayoutEffect mount "one"',
132 + ]);
133 + } else {
134 + expect(Scheduler).toFlushUntilNextPaint([
135 + 'useLayoutEffect mount "one"',
136 + ]);
137 + }
138 + });
139 +
140 + ReactNoop.act(() => {
141 + ReactNoop.renderLegacySyncRoot(
142 + <React.StrictMode>
143 + <ComponentWithEffects label={'one'} />
144 + <ComponentWithEffects label={'two'} />
145 + </React.StrictMode>,
146 + );
147 +
148 + if (shouldDoubleInvokingEffects()) {
149 + expect(Scheduler).toFlushUntilNextPaint([
150 + // Cleanup and re-run "one" (and "two") since there is no dependencies array.
151 + 'useLayoutEffect unmount "one"',
152 + 'useLayoutEffect mount "one"',
153 + 'useLayoutEffect mount "two"',
154 +
155 + // Since "two" is new, it should be double-invoked.
156 + 'useLayoutEffect unmount "two"',
157 + 'useLayoutEffect mount "two"',
158 + ]);
159 + } else {
160 + expect(Scheduler).toFlushUntilNextPaint([
161 + 'useLayoutEffect unmount "one"',
162 + 'useLayoutEffect mount "one"',
163 + 'useLayoutEffect mount "two"',
164 + ]);
165 + }
166 + });
167 + });
168 +
169 + // This test also verifies that double-invoked effects flush synchronously
170 + // within the same frame as passive effects.
171 + it('should double invoke effects only for newly mounted components', () => {
172 + function ComponentWithEffects({label}) {
173 + React.useEffect(() => {
174 + Scheduler.unstable_yieldValue(`useEffect mount "${label}"`);
175 + return () =>
176 + Scheduler.unstable_yieldValue(`useEffect unmount "${label}"`);
177 + });
178 +
179 + React.useLayoutEffect(() => {
180 + Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`);
181 + return () =>
182 + Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`);
183 + });
184 +
185 + return label;
186 + }
187 +
188 + ReactNoop.act(() => {
189 + ReactNoop.renderLegacySyncRoot(
190 + <React.StrictMode>
191 + <ComponentWithEffects label={'one'} />
192 + </React.StrictMode>,
193 + );
194 +
195 + if (shouldDoubleInvokingEffects()) {
196 + expect(Scheduler).toFlushAndYieldThrough([
197 + 'useLayoutEffect mount "one"',
198 + ]);
199 + expect(Scheduler).toFlushAndYield([
200 + 'useEffect mount "one"',
201 + 'useLayoutEffect unmount "one"',
202 + 'useEffect unmount "one"',
203 + 'useLayoutEffect mount "one"',
204 + 'useEffect mount "one"',
205 + ]);
206 + } else {
207 + expect(Scheduler).toFlushAndYieldThrough([
208 + 'useLayoutEffect mount "one"',
209 + ]);
210 + expect(Scheduler).toFlushAndYield(['useEffect mount "one"']);
211 + }
212 + });
213 +
214 + ReactNoop.act(() => {
215 + ReactNoop.renderLegacySyncRoot(
216 + <React.StrictMode>
217 + <ComponentWithEffects label={'one'} />
218 + <ComponentWithEffects label={'two'} />
219 + </React.StrictMode>,
220 + );
221 +
222 + if (shouldDoubleInvokingEffects()) {
223 + expect(Scheduler).toFlushAndYieldThrough([
224 + // Cleanup and re-run "one" (and "two") since there is no dependencies array.
225 + 'useLayoutEffect unmount "one"',
226 + 'useLayoutEffect mount "one"',
227 + 'useLayoutEffect mount "two"',
228 + ]);
229 + expect(Scheduler).toFlushAndYield([
230 + 'useEffect unmount "one"',
231 + 'useEffect mount "one"',
232 + 'useEffect mount "two"',
233 +
234 + // Since "two" is new, it should be double-invoked.
235 + 'useLayoutEffect unmount "two"',
236 + 'useEffect unmount "two"',
237 + 'useLayoutEffect mount "two"',
238 + 'useEffect mount "two"',
239 + ]);
240 + } else {
241 + expect(Scheduler).toFlushAndYieldThrough([
242 + 'useLayoutEffect unmount "one"',
243 + 'useLayoutEffect mount "one"',
244 + 'useLayoutEffect mount "two"',
245 + ]);
246 + expect(Scheduler).toFlushAndYield([
247 + 'useEffect unmount "one"',
248 + 'useEffect mount "one"',
249 + 'useEffect mount "two"',
250 + ]);
251 + }
252 + });
253 + });
254 +
255 + it('double invoking for effects for modern roots', () => {
256 + function App({text}) {
257 + React.useEffect(() => {
258 + Scheduler.unstable_yieldValue('useEffect mount');
259 + return () => Scheduler.unstable_yieldValue('useEffect unmount');
260 + });
261 +
262 + React.useLayoutEffect(() => {
263 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
264 + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
265 + });
266 +
267 + return text;
268 + }
269 + ReactNoop.act(() => {
270 + ReactNoop.render(<App text={'mount'} />);
271 + });
272 +
273 + if (shouldDoubleInvokingEffects()) {
274 + expect(Scheduler).toHaveYielded([
275 + 'useLayoutEffect mount',
276 + 'useEffect mount',
277 + 'useLayoutEffect unmount',
278 + 'useEffect unmount',
279 + 'useLayoutEffect mount',
280 + 'useEffect mount',
281 + ]);
282 + } else {
283 + expect(Scheduler).toHaveYielded([
284 + 'useLayoutEffect mount',
285 + 'useEffect mount',
286 + ]);
287 + }
288 +
289 + ReactNoop.act(() => {
290 + ReactNoop.render(<App text={'update'} />);
291 + });
292 +
293 + expect(Scheduler).toHaveYielded([
294 + 'useLayoutEffect unmount',
295 + 'useLayoutEffect mount',
296 + 'useEffect unmount',
297 + 'useEffect mount',
298 + ]);
299 +
300 + ReactNoop.act(() => {
301 + ReactNoop.render(null);
302 + });
303 +
304 + expect(Scheduler).toHaveYielded([
305 + 'useLayoutEffect unmount',
306 + 'useEffect unmount',
307 + ]);
308 + });
309 +
310 + it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => {
311 + function App({text}) {
312 + React.useEffect(() => {
313 + Scheduler.unstable_yieldValue('useEffect One mount');
314 + return () => Scheduler.unstable_yieldValue('useEffect One unmount');
315 + });
316 +
317 + React.useEffect(() => {
318 + Scheduler.unstable_yieldValue('useEffect Two mount');
319 + return () => Scheduler.unstable_yieldValue('useEffect Two unmount');
320 + });
321 +
322 + return text;
323 + }
324 +
325 + ReactNoop.act(() => {
326 + ReactNoop.render(<App text={'mount'} />);
327 + });
328 +
329 + if (shouldDoubleInvokingEffects()) {
330 + expect(Scheduler).toHaveYielded([
331 + 'useEffect One mount',
332 + 'useEffect Two mount',
333 + 'useEffect One unmount',
334 + 'useEffect Two unmount',
335 + 'useEffect One mount',
336 + 'useEffect Two mount',
337 + ]);
338 + } else {
339 + expect(Scheduler).toHaveYielded([
340 + 'useEffect One mount',
341 + 'useEffect Two mount',
342 + ]);
343 + }
344 +
345 + ReactNoop.act(() => {
346 + ReactNoop.render(<App text={'update'} />);
347 + });
348 +
349 + expect(Scheduler).toHaveYielded([
350 + 'useEffect One unmount',
351 + 'useEffect Two unmount',
352 + 'useEffect One mount',
353 + 'useEffect Two mount',
354 + ]);
355 +
356 + ReactNoop.act(() => {
357 + ReactNoop.render(null);
358 + });
359 +
360 + expect(Scheduler).toHaveYielded([
361 + 'useEffect One unmount',
362 + 'useEffect Two unmount',
363 + ]);
364 + });
365 +
366 + it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => {
367 + function App({text}) {
368 + React.useLayoutEffect(() => {
369 + Scheduler.unstable_yieldValue('useLayoutEffect One mount');
370 + return () =>
371 + Scheduler.unstable_yieldValue('useLayoutEffect One unmount');
372 + });
373 +
374 + React.useLayoutEffect(() => {
375 + Scheduler.unstable_yieldValue('useLayoutEffect Two mount');
376 + return () =>
377 + Scheduler.unstable_yieldValue('useLayoutEffect Two unmount');
378 + });
379 +
380 + return text;
381 + }
382 +
383 + ReactNoop.act(() => {
384 + ReactNoop.render(<App text={'mount'} />);
385 + });
386 +
387 + if (shouldDoubleInvokingEffects()) {
388 + expect(Scheduler).toHaveYielded([
389 + 'useLayoutEffect One mount',
390 + 'useLayoutEffect Two mount',
391 + 'useLayoutEffect One unmount',
392 + 'useLayoutEffect Two unmount',
393 + 'useLayoutEffect One mount',
394 + 'useLayoutEffect Two mount',
395 + ]);
396 + } else {
397 + expect(Scheduler).toHaveYielded([
398 + 'useLayoutEffect One mount',
399 + 'useLayoutEffect Two mount',
400 + ]);
401 + }
402 +
403 + ReactNoop.act(() => {
404 + ReactNoop.render(<App text={'update'} />);
405 + });
406 +
407 + expect(Scheduler).toHaveYielded([
408 + 'useLayoutEffect One unmount',
409 + 'useLayoutEffect Two unmount',
410 + 'useLayoutEffect One mount',
411 + 'useLayoutEffect Two mount',
412 + ]);
413 +
414 + ReactNoop.act(() => {
415 + ReactNoop.render(null);
416 + });
417 +
418 + expect(Scheduler).toHaveYielded([
419 + 'useLayoutEffect One unmount',
420 + 'useLayoutEffect Two unmount',
421 + ]);
422 + });
423 +
424 + it('useEffect and useLayoutEffect is called twice when there is no unmount', () => {
425 + function App({text}) {
426 + React.useEffect(() => {
427 + Scheduler.unstable_yieldValue('useEffect mount');
428 + });
429 +
430 + React.useLayoutEffect(() => {
431 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
432 + });
433 +
434 + return text;
435 + }
436 +
437 + ReactNoop.act(() => {
438 + ReactNoop.render(<App text={'mount'} />);
439 + });
440 +
441 + if (shouldDoubleInvokingEffects()) {
442 + expect(Scheduler).toHaveYielded([
443 + 'useLayoutEffect mount',
444 + 'useEffect mount',
445 + 'useLayoutEffect mount',
446 + 'useEffect mount',
447 + ]);
448 + } else {
449 + expect(Scheduler).toHaveYielded([
450 + 'useLayoutEffect mount',
451 + 'useEffect mount',
452 + ]);
453 + }
454 +
455 + ReactNoop.act(() => {
456 + ReactNoop.render(<App text={'update'} />);
457 + });
458 +
459 + expect(Scheduler).toHaveYielded([
460 + 'useLayoutEffect mount',
461 + 'useEffect mount',
462 + ]);
463 +
464 + ReactNoop.act(() => {
465 + ReactNoop.render(null);
466 + });
467 +
468 + expect(Scheduler).toHaveYielded([]);
469 + });
470 +
471 + it('passes the right context to class component lifecycles', () => {
472 + class App extends React.PureComponent {
473 + test() {}
474 +
475 + componentDidMount() {
476 + this.test();
477 + Scheduler.unstable_yieldValue('componentDidMount');
478 + }
479 +
480 + componentDidUpdate() {
481 + this.test();
482 + Scheduler.unstable_yieldValue('componentDidUpdate');
483 + }
484 +
485 + componentWillUnmount() {
486 + this.test();
487 + Scheduler.unstable_yieldValue('componentWillUnmount');
488 + }
489 +
490 + render() {
491 + return null;
492 + }
493 + }
494 +
495 + ReactNoop.act(() => {
496 + ReactNoop.render(<App />);
497 + });
498 +
499 + if (shouldDoubleInvokingEffects()) {
500 + expect(Scheduler).toHaveYielded([
501 + 'componentDidMount',
502 + 'componentWillUnmount',
503 + 'componentDidMount',
504 + ]);
505 + } else {
506 + expect(Scheduler).toHaveYielded(['componentDidMount']);
507 + }
508 + });
509 +
510 + it('double invoking works for class components', () => {
511 + class App extends React.PureComponent {
512 + componentDidMount() {
513 + Scheduler.unstable_yieldValue('componentDidMount');
514 + }
515 +
516 + componentDidUpdate() {
517 + Scheduler.unstable_yieldValue('componentDidUpdate');
518 + }
519 +
520 + componentWillUnmount() {
521 + Scheduler.unstable_yieldValue('componentWillUnmount');
522 + }
523 +
524 + render() {
525 + return this.props.text;
526 + }
527 + }
528 +
529 + ReactNoop.act(() => {
530 + ReactNoop.render(<App text={'mount'} />);
531 + });
532 +
533 + if (shouldDoubleInvokingEffects()) {
534 + expect(Scheduler).toHaveYielded([
535 + 'componentDidMount',
536 + 'componentWillUnmount',
537 + 'componentDidMount',
538 + ]);
539 + } else {
540 + expect(Scheduler).toHaveYielded(['componentDidMount']);
541 + }
542 +
543 + ReactNoop.act(() => {
544 + ReactNoop.render(<App text={'update'} />);
545 + });
546 +
547 + expect(Scheduler).toHaveYielded(['componentDidUpdate']);
548 +
549 + ReactNoop.act(() => {
550 + ReactNoop.render(null);
551 + });
552 +
553 + expect(Scheduler).toHaveYielded(['componentWillUnmount']);
554 + });
555 +
556 + it('double flushing passive effects only results in one double invoke', () => {
557 + function App({text}) {
558 + const [state, setState] = React.useState(0);
559 + React.useEffect(() => {
560 + if (state !== 1) {
561 + setState(1);
562 + }
563 + Scheduler.unstable_yieldValue('useEffect mount');
564 + return () => Scheduler.unstable_yieldValue('useEffect unmount');
565 + });
566 +
567 + React.useLayoutEffect(() => {
568 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
569 + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
570 + });
571 +
572 + Scheduler.unstable_yieldValue(text);
573 + return text;
574 + }
575 +
576 + ReactNoop.act(() => {
577 + ReactNoop.render(<App text={'mount'} />);
578 + });
579 +
580 + if (shouldDoubleInvokingEffects()) {
581 + expect(Scheduler).toHaveYielded([
582 + 'mount',
583 + 'useLayoutEffect mount',
584 + 'useEffect mount',
585 + 'useLayoutEffect unmount',
586 + 'useEffect unmount',
587 + 'useLayoutEffect mount',
588 + 'useEffect mount',
589 + 'mount',
590 + 'useLayoutEffect unmount',
591 + 'useLayoutEffect mount',
592 + 'useEffect unmount',
593 + 'useEffect mount',
594 + ]);
595 + } else {
596 + expect(Scheduler).toHaveYielded([
597 + 'mount',
598 + 'useLayoutEffect mount',
599 + 'useEffect mount',
600 + 'mount',
601 + 'useLayoutEffect unmount',
602 + 'useLayoutEffect mount',
603 + 'useEffect unmount',
604 + 'useEffect mount',
605 + ]);
606 + }
607 + });
608 +
609 + it('newly mounted components after initial mount get double invoked', () => {
610 + let _setShowChild;
611 + function Child() {
612 + React.useEffect(() => {
613 + Scheduler.unstable_yieldValue('Child useEffect mount');
614 + return () => Scheduler.unstable_yieldValue('Child useEffect unmount');
615 + });
616 + React.useLayoutEffect(() => {
617 + Scheduler.unstable_yieldValue('Child useLayoutEffect mount');
618 + return () =>
619 + Scheduler.unstable_yieldValue('Child useLayoutEffect unmount');
620 + });
621 +
622 + return null;
623 + }
624 +
625 + function App() {
626 + const [showChild, setShowChild] = React.useState(false);
627 + _setShowChild = setShowChild;
628 + React.useEffect(() => {
629 + Scheduler.unstable_yieldValue('App useEffect mount');
630 + return () => Scheduler.unstable_yieldValue('App useEffect unmount');
631 + });
632 + React.useLayoutEffect(() => {
633 + Scheduler.unstable_yieldValue('App useLayoutEffect mount');
634 + return () =>
635 + Scheduler.unstable_yieldValue('App useLayoutEffect unmount');
636 + });
637 +
638 + return showChild && <Child />;
639 + }
640 +
641 + ReactNoop.act(() => {
642 + ReactNoop.render(<App />);
643 + });
644 +
645 + if (shouldDoubleInvokingEffects()) {
646 + expect(Scheduler).toHaveYielded([
647 + 'App useLayoutEffect mount',
648 + 'App useEffect mount',
649 + 'App useLayoutEffect unmount',
650 + 'App useEffect unmount',
651 + 'App useLayoutEffect mount',
652 + 'App useEffect mount',
653 + ]);
654 + } else {
655 + expect(Scheduler).toHaveYielded([
656 + 'App useLayoutEffect mount',
657 + 'App useEffect mount',
658 + ]);
659 + }
660 +
661 + ReactNoop.act(() => {
662 + _setShowChild(true);
663 + });
664 +
665 + if (shouldDoubleInvokingEffects()) {
666 + expect(Scheduler).toHaveYielded([
667 + 'App useLayoutEffect unmount',
668 + 'Child useLayoutEffect mount',
669 + 'App useLayoutEffect mount',
670 + 'App useEffect unmount',
671 + 'Child useEffect mount',
672 + 'App useEffect mount',
673 + 'Child useLayoutEffect unmount',
674 + 'Child useEffect unmount',
675 + 'Child useLayoutEffect mount',
676 + 'Child useEffect mount',
677 + ]);
678 + } else {
679 + expect(Scheduler).toHaveYielded([
680 + 'App useLayoutEffect unmount',
681 + 'Child useLayoutEffect mount',
682 + 'App useLayoutEffect mount',
683 + 'App useEffect unmount',
684 + 'Child useEffect mount',
685 + 'App useEffect mount',
686 + ]);
687 + }
688 + });
689 +
690 + it('classes and functions are double invoked together correctly', () => {
691 + class ClassChild extends React.PureComponent {
692 + componentDidMount() {
693 + Scheduler.unstable_yieldValue('componentDidMount');
694 + }
695 +
696 + componentWillUnmount() {
697 + Scheduler.unstable_yieldValue('componentWillUnmount');
698 + }
699 +
700 + render() {
701 + return this.props.text;
702 + }
703 + }
704 +
705 + function FunctionChild({text}) {
706 + React.useEffect(() => {
707 + Scheduler.unstable_yieldValue('useEffect mount');
708 + return () => Scheduler.unstable_yieldValue('useEffect unmount');
709 + });
710 + React.useLayoutEffect(() => {
711 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
712 + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
713 + });
714 + return text;
715 + }
716 +
717 + function App({text}) {
718 + return (
719 + <>
720 + <ClassChild text={text} />
721 + <FunctionChild text={text} />
722 + </>
723 + );
724 + }
725 +
726 + ReactNoop.act(() => {
727 + ReactNoop.render(<App text={'mount'} />);
728 + });
729 +
730 + if (shouldDoubleInvokingEffects()) {
731 + expect(Scheduler).toHaveYielded([
732 + 'componentDidMount',
733 + 'useLayoutEffect mount',
734 + 'useEffect mount',
735 + 'componentWillUnmount',
736 + 'useLayoutEffect unmount',
737 + 'useEffect unmount',
738 + 'componentDidMount',
739 + 'useLayoutEffect mount',
740 + 'useEffect mount',
741 + ]);
742 + } else {
743 + expect(Scheduler).toHaveYielded([
744 + 'componentDidMount',
745 + 'useLayoutEffect mount',
746 + 'useEffect mount',
747 + ]);
748 + }
749 +
750 + ReactNoop.act(() => {
751 + ReactNoop.render(<App text={'mount'} />);
752 + });
753 +
754 + expect(Scheduler).toHaveYielded([
755 + 'useLayoutEffect unmount',
756 + 'useLayoutEffect mount',
757 + 'useEffect unmount',
758 + 'useEffect mount',
759 + ]);
760 +
761 + ReactNoop.act(() => {
762 + ReactNoop.render(null);
763 + });
764 +
765 + expect(Scheduler).toHaveYielded([
766 + 'componentWillUnmount',
767 + 'useLayoutEffect unmount',
768 + 'useEffect unmount',
769 + ]);
770 + });
771 +});
packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.js
+1 -3
@@ -26,9 +26,7 @@ describe('ReactDoubleInvokeEvents', () => {
26 function supportsDoubleInvokeEffects() {
27 return gate(
28 flags =>
29 - flags.build === 'development' &&
30 - flags.enableDoubleInvokingEffects &&
31 - flags.dfsEffectsRefactor,
29 + flags.build === 'development' && flags.enableDoubleInvokingEffects,
30 );
31 }
32
packages/react/src/__tests__/ReactProfiler-test.internal.js
+6 -4
@@ -4874,10 +4874,8 @@ describe('Profiler', () => {
4874 });
4875
4876 if (__DEV__) {
4877 - // @gate dfsEffectsRefactor
4878 - // @gate enableDoubleInvokingEffects
4877 it('double invoking does not disconnect wrapped async work', () => {
4880 - ReactFeatureFlags.enableDoubleInvokingEffects = true;
4878 + ReactFeatureFlags.enableDoubleInvokingEffects = !__VARIANT__;
4879
4880 const callback = jest.fn(() => {
4881 const wrappedInteractions = SchedulerTracing.unstable_getCurrent();
@@ -4915,7 +4913,11 @@ describe('Profiler', () => {
4913
4914 jest.runAllTimers();
4915
4918 - expect(callback).toHaveBeenCalledTimes(4); // 2x per effect
4916 + if (ReactFeatureFlags.enableDoubleInvokingEffects) {
4917 + expect(callback).toHaveBeenCalledTimes(4); // 2x per effect
4918 + } else {
4919 + expect(callback).toHaveBeenCalledTimes(2);
4920 + }
4921
4922 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
4923 });