@samitouri / QOS-React / commits / c63741fb3d

offscreen double invoke effects (#19523)

This PR double invokes effects in __DEV__ mode. We are thinking about unmounting layout and/or passive effects for a hidden tree. To understand potential issues with this, we want to double invoke effects. This PR changes the behavior in DEV when an effect runs from create() to create() -> destroy() -> create(). The effect cleanup function will still be called before the effect runs in both dev and prod. (Note: This change is purely for research for now as it is likely to break real code.) **Note: The change is fully behind a flag and does not affect any of the code on npm.**

Luna Ruan committed Sep 24, 2020 at 13:42 UTC c63741fb3daef6c1e8746cbe7d7b07ecb281a9fd
17 files changed +888 -60
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+22 -5
@@ -12,13 +12,14 @@ import type {Lanes} from './ReactFiberLane';
12 import type {UpdateQueue} from './ReactUpdateQueue.new';
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.new';
25 import {isMounted} from './ReactFiberTreeReflection';
@@ -890,7 +891,11 @@ function mountClassInstance(
891 }
892
893 if (typeof instance.componentDidMount === 'function') {
893 - workInProgress.flags |= Update;
894 + if (__DEV__ && enableDoubleInvokingEffects) {
895 + workInProgress.flags |= MountLayoutDev | Update;
896 + } else {
897 + workInProgress.flags |= Update;
898 + }
899 }
900 }
901
@@ -960,7 +965,11 @@ function resumeMountClassInstance(
965 // If an update was already in progress, we should schedule an Update
966 // effect even though we're bailing out, so that cWU/cDU are called.
967 if (typeof instance.componentDidMount === 'function') {
963 - workInProgress.flags |= Update;
968 + if (__DEV__ && enableDoubleInvokingEffects) {
969 + workInProgress.flags |= MountLayoutDev | Update;
970 + } else {
971 + workInProgress.flags |= Update;
972 + }
973 }
974 return false;
975 }
@@ -1003,13 +1012,21 @@ function resumeMountClassInstance(
1012 }
1013 }
1014 if (typeof instance.componentDidMount === 'function') {
1006 - workInProgress.flags |= Update;
1015 + if (__DEV__ && enableDoubleInvokingEffects) {
1016 + workInProgress.flags |= MountLayoutDev | Update;
1017 + } else {
1018 + workInProgress.flags |= Update;
1019 + }
1020 }
1021 } else {
1022 // If an update was already in progress, we should schedule an Update
1023 // effect even though we're bailing out, so that cWU/cDU are called.
1024 if (typeof instance.componentDidMount === 'function') {
1012 - workInProgress.flags |= Update;
1025 + if (__DEV__ && enableDoubleInvokingEffects) {
1026 + workInProgress.flags |= MountLayoutDev | Update;
1027 + } else {
1028 + workInProgress.flags |= Update;
1029 + }
1030 }
1031
1032 // If shouldComponentUpdate returned false, we should still update the
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+135 -5
@@ -35,6 +35,7 @@ import {
35 enableFundamentalAPI,
36 enableSuspenseCallback,
37 enableScopeAPI,
38 + enableDoubleInvokingEffects,
39 } from 'shared/ReactFeatureFlags';
40 import {
41 FunctionComponent,
@@ -159,7 +160,7 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
160 function safelyCallComponentWillUnmount(
161 current: Fiber,
162 instance: any,
162 - nearestMountedAncestor: Fiber,
163 + nearestMountedAncestor: Fiber | null,
164 ) {
165 if (__DEV__) {
166 invokeGuardedCallback(
@@ -318,7 +319,7 @@ function commitBeforeMutationLifeCycles(
319 }
320
321 function commitHookEffectListUnmount(
321 - tag: HookFlags,
322 + flags: HookFlags,
323 finishedWork: Fiber,
324 nearestMountedAncestor: Fiber | null,
325 ) {
@@ -328,7 +329,7 @@ function commitHookEffectListUnmount(
329 const firstEffect = lastEffect.next;
330 let effect = firstEffect;
331 do {
331 - if ((effect.tag & tag) === tag) {
332 + if ((effect.tag & flags) === flags) {
333 // Unmount
334 const destroy = effect.destroy;
335 effect.destroy = undefined;
@@ -341,14 +342,14 @@ function commitHookEffectListUnmount(
342 }
343 }
344
344 -function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
345 +function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
346 const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
347 const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
348 if (lastEffect !== null) {
349 const firstEffect = lastEffect.next;
350 let effect = firstEffect;
351 do {
351 - if ((effect.tag & tag) === tag) {
352 + if ((effect.tag & flags) === flags) {
353 // Mount
354 const create = effect.create;
355 effect.destroy = create();
@@ -1884,6 +1885,131 @@ function commitPassiveMount(
1885 }
1886 }
1887
1888 +function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
1889 + if (__DEV__ && enableDoubleInvokingEffects) {
1890 + switch (fiber.tag) {
1891 + case FunctionComponent:
1892 + case ForwardRef:
1893 + case SimpleMemoComponent:
1894 + case Block: {
1895 + invokeGuardedCallback(
1896 + null,
1897 + commitHookEffectListMount,
1898 + null,
1899 + HookLayout | HookHasEffect,
1900 + fiber,
1901 + );
1902 + if (hasCaughtError()) {
1903 + const mountError = clearCaughtError();
1904 + captureCommitPhaseError(fiber, fiber.return, mountError);
1905 + }
1906 + break;
1907 + }
1908 + case ClassComponent: {
1909 + const instance = fiber.stateNode;
1910 + invokeGuardedCallback(null, instance.componentDidMount, null);
1911 + if (hasCaughtError()) {
1912 + const mountError = clearCaughtError();
1913 + captureCommitPhaseError(fiber, fiber.return, mountError);
1914 + }
1915 + break;
1916 + }
1917 + }
1918 + }
1919 +}
1920 +
1921 +function invokePassiveEffectMountInDEV(fiber: Fiber): void {
1922 + if (__DEV__ && enableDoubleInvokingEffects) {
1923 + switch (fiber.tag) {
1924 + case FunctionComponent:
1925 + case ForwardRef:
1926 + case SimpleMemoComponent:
1927 + case Block: {
1928 + invokeGuardedCallback(
1929 + null,
1930 + commitHookEffectListMount,
1931 + null,
1932 + HookPassive | HookHasEffect,
1933 + fiber,
1934 + );
1935 + if (hasCaughtError()) {
1936 + const mountError = clearCaughtError();
1937 + captureCommitPhaseError(fiber, fiber.return, mountError);
1938 + }
1939 + break;
1940 + }
1941 + }
1942 + }
1943 +}
1944 +
1945 +function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
1946 + if (__DEV__ && enableDoubleInvokingEffects) {
1947 + switch (fiber.tag) {
1948 + case FunctionComponent:
1949 + case ForwardRef:
1950 + case SimpleMemoComponent:
1951 + case Block: {
1952 + invokeGuardedCallback(
1953 + null,
1954 + commitHookEffectListUnmount,
1955 + null,
1956 + HookLayout | HookHasEffect,
1957 + fiber,
1958 + fiber.return,
1959 + );
1960 + if (hasCaughtError()) {
1961 + const unmountError = clearCaughtError();
1962 + captureCommitPhaseError(fiber, fiber.return, unmountError);
1963 + }
1964 + break;
1965 + }
1966 + case ClassComponent: {
1967 + const instance = fiber.stateNode;
1968 + if (typeof instance.componentWillUnmount === 'function') {
1969 + invokeGuardedCallback(
1970 + null,
1971 + safelyCallComponentWillUnmount,
1972 + null,
1973 + fiber,
1974 + instance,
1975 + fiber.return,
1976 + );
1977 + if (hasCaughtError()) {
1978 + const unmountError = clearCaughtError();
1979 + captureCommitPhaseError(fiber, fiber.return, unmountError);
1980 + }
1981 + }
1982 + break;
1983 + }
1984 + }
1985 + }
1986 +}
1987 +
1988 +function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
1989 + if (__DEV__ && enableDoubleInvokingEffects) {
1990 + switch (fiber.tag) {
1991 + case FunctionComponent:
1992 + case ForwardRef:
1993 + case SimpleMemoComponent:
1994 + case Block: {
1995 + invokeGuardedCallback(
1996 + null,
1997 + commitHookEffectListUnmount,
1998 + null,
1999 + HookPassive | HookHasEffect,
2000 + fiber,
2001 + fiber.return,
2002 + );
2003 + if (hasCaughtError()) {
2004 + const unmountError = clearCaughtError();
2005 + captureCommitPhaseError(fiber, fiber.return, unmountError);
2006 + }
2007 + break;
2008 + }
2009 + }
2010 + }
2011 +}
2012 +
2013 export {
2014 commitBeforeMutationLifeCycles,
2015 commitResetTextContent,
@@ -1896,4 +2022,8 @@ export {
2022 commitPassiveUnmount,
2023 commitPassiveUnmountInsideDeletedTree,
2024 commitPassiveMount,
2025 + invokeLayoutEffectMountInDEV,
2026 + invokeLayoutEffectUnmountInDEV,
2027 + invokePassiveEffectMountInDEV,
2028 + invokePassiveEffectUnmountInDEV,
2029 };
packages/react-reconciler/src/ReactFiberFlags.js
+32 -26
@@ -10,50 +10,56 @@
10 export type Flags = number;
11
12 // Don't change these two values. They're used by React Dev Tools.
13 -export const NoFlags = /* */ 0b0000000000000000;
14 -export const PerformedWork = /* */ 0b0000000000000001;
13 +export const NoFlags = /* */ 0b000000000000000000;
14 +export const PerformedWork = /* */ 0b000000000000000001;
15
16 // You can change the rest (and add more).
17 -export const Placement = /* */ 0b0000000000000010;
18 -export const Update = /* */ 0b0000000000000100;
19 -export const PlacementAndUpdate = /* */ 0b0000000000000110;
20 -export const Deletion = /* */ 0b0000000000001000;
21 -export const ContentReset = /* */ 0b0000000000010000;
22 -export const Callback = /* */ 0b0000000000100000;
23 -export const DidCapture = /* */ 0b0000000001000000;
24 -export const Ref = /* */ 0b0000000010000000;
25 -export const Snapshot = /* */ 0b0000000100000000;
26 -export const Passive = /* */ 0b0000001000000000;
17 +export const Placement = /* */ 0b000000000000000010;
18 +export const Update = /* */ 0b000000000000000100;
19 +export const PlacementAndUpdate = /* */ 0b000000000000000110;
20 +export const Deletion = /* */ 0b000000000000001000;
21 +export const ContentReset = /* */ 0b000000000000010000;
22 +export const Callback = /* */ 0b000000000000100000;
23 +export const DidCapture = /* */ 0b000000000001000000;
24 +export const Ref = /* */ 0b000000000010000000;
25 +export const Snapshot = /* */ 0b000000000100000000;
26 +export const Passive = /* */ 0b000000001000000000;
27 // TODO (effects) Remove this bit once the new reconciler is synced to the old.
28 -export const PassiveUnmountPendingDev = /* */ 0b0010000000000000;
29 -export const Hydrating = /* */ 0b0000010000000000;
30 -export const HydratingAndUpdate = /* */ 0b0000010000000100;
28 +export const PassiveUnmountPendingDev = /* */ 0b000010000000000000;
29 +export const Hydrating = /* */ 0b000000010000000000;
30 +export const HydratingAndUpdate = /* */ 0b000000010000000100;
31
32 // Passive & Update & Callback & Ref & Snapshot
33 -export const LifecycleEffectMask = /* */ 0b0000001110100100;
33 +export const LifecycleEffectMask = /* */ 0b000000001110100100;
34
35 // Union of all host effects
36 -export const HostEffectMask = /* */ 0b0000011111111111;
36 +export const HostEffectMask = /* */ 0b000000011111111111;
37
38 // These are not really side effects, but we still reuse this field.
39 -export const Incomplete = /* */ 0b0000100000000000;
40 -export const ShouldCapture = /* */ 0b0001000000000000;
41 -export const ForceUpdateForLegacySuspense = /* */ 0b0100000000000000;
39 +export const Incomplete = /* */ 0b000000100000000000;
40 +export const ShouldCapture = /* */ 0b000001000000000000;
41 +export const ForceUpdateForLegacySuspense = /* */ 0b000100000000000000;
42
43 // Static tags describe aspects of a fiber that are not specific to a render,
44 // e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
45 // This enables us to defer more work in the unmount case,
46 // since we can defer traversing the tree during layout to look for Passive effects,
47 // and instead rely on the static flag as a signal that there may be cleanup work.
48 -export const PassiveStatic = /* */ 0b1000000000000000;
48 +export const PassiveStatic = /* */ 0b001000000000000000;
49
50 // Union of side effect groupings as pertains to subtreeFlags
51 -export const BeforeMutationMask = /* */ 0b0000001100001010;
52 -export const MutationMask = /* */ 0b0000010010011110;
53 -export const LayoutMask = /* */ 0b0000000010100100;
54 -export const PassiveMask = /* */ 0b0000001000001000;
51 +export const BeforeMutationMask = /* */ 0b000000001100001010;
52 +export const MutationMask = /* */ 0b000000010010011110;
53 +export const LayoutMask = /* */ 0b000000000010100100;
54 +export const PassiveMask = /* */ 0b000000001000001000;
55
56 // Union of tags that don't get reset on clones.
57 // This allows certain concepts to persist without recalculting them,
58 // e.g. whether a subtree contains passive effects or portals.
59 -export const StaticMask = /* */ 0b1000000000000000;
59 +export const StaticMask = /* */ 0b001000000000000000;
60 +
61 +// These flags allow us to traverse to fibers that have effects on mount
62 +// without traversing the entire tree after every commit for
63 +// double invoking
64 +export const MountLayoutDev = /* */ 0b010000000000000000;
65 +export const MountPassiveDev = /* */ 0b100000000000000000;
packages/react-reconciler/src/ReactFiberHooks.new.js
+60 -15
@@ -26,6 +26,7 @@ import {
26 enableSchedulingProfiler,
27 enableNewReconciler,
28 decoupleUpdatePriorityFromScheduler,
29 + enableDoubleInvokingEffects,
30 } from 'shared/ReactFeatureFlags';
31
32 import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
@@ -48,6 +49,8 @@ import {
49 Update as UpdateEffect,
50 Passive as PassiveEffect,
51 PassiveStatic as PassiveStaticEffect,
52 + MountLayoutDev as MountLayoutDevEffect,
53 + MountPassiveDev as MountPassiveDevEffect,
54 } from './ReactFiberFlags';
55 import {
56 HasEffect as HookHasEffect,
@@ -482,7 +485,16 @@ export function bailoutHooks(
485 lanes: Lanes,
486 ) {
487 workInProgress.updateQueue = current.updateQueue;
485 - workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
488 + if (__DEV__ && enableDoubleInvokingEffects) {
489 + workInProgress.flags &= ~(
490 + MountPassiveDevEffect |
491 + PassiveEffect |
492 + MountLayoutDevEffect |
493 + UpdateEffect
494 + );
495 + } else {
496 + workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
497 + }
498 current.lanes = removeLanes(current.lanes, lanes);
499 }
500
@@ -1240,12 +1252,22 @@ function mountEffect(
1252 warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1253 }
1254 }
1243 - return mountEffectImpl(
1244 - PassiveEffect | PassiveStaticEffect,
1245 - HookPassive,
1246 - create,
1247 - deps,
1248 - );
1255 +
1256 + if (__DEV__ && enableDoubleInvokingEffects) {
1257 + return mountEffectImpl(
1258 + MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
1259 + HookPassive,
1260 + create,
1261 + deps,
1262 + );
1263 + } else {
1264 + return mountEffectImpl(
1265 + PassiveEffect | PassiveStaticEffect,
1266 + HookPassive,
1267 + create,
1268 + deps,
1269 + );
1270 + }
1271 }
1272
1273 function updateEffect(
@@ -1265,7 +1287,16 @@ function mountLayoutEffect(
1287 create: () => (() => void) | void,
1288 deps: Array<mixed> | void | null,
1289 ): void {
1268 - return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1290 + if (__DEV__ && enableDoubleInvokingEffects) {
1291 + return mountEffectImpl(
1292 + MountLayoutDevEffect | UpdateEffect,
1293 + HookLayout,
1294 + create,
1295 + deps,
1296 + );
1297 + } else {
1298 + return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1299 + }
1300 }
1301
1302 function updateLayoutEffect(
@@ -1324,12 +1355,21 @@ function mountImperativeHandle<T>(
1355 const effectDeps =
1356 deps !== null && deps !== undefined ? deps.concat([ref]) : null;
1357
1327 - return mountEffectImpl(
1328 - UpdateEffect,
1329 - HookLayout,
1330 - imperativeHandleEffect.bind(null, create, ref),
1331 - effectDeps,
1332 - );
1358 + if (__DEV__ && enableDoubleInvokingEffects) {
1359 + return mountEffectImpl(
1360 + MountLayoutDevEffect | UpdateEffect,
1361 + HookLayout,
1362 + imperativeHandleEffect.bind(null, create, ref),
1363 + effectDeps,
1364 + );
1365 + } else {
1366 + return mountEffectImpl(
1367 + UpdateEffect,
1368 + HookLayout,
1369 + imperativeHandleEffect.bind(null, create, ref),
1370 + effectDeps,
1371 + );
1372 + }
1373 }
1374
1375 function updateImperativeHandle<T>(
@@ -1610,7 +1650,12 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
1650 const setId = mountState(id)[1];
1651
1652 if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1613 - currentlyRenderingFiber.flags |= PassiveEffect | PassiveStaticEffect;
1653 + if (__DEV__ && enableDoubleInvokingEffects) {
1654 + currentlyRenderingFiber.flags |=
1655 + MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect;
1656 + } else {
1657 + currentlyRenderingFiber.flags |= PassiveEffect | PassiveStaticEffect;
1658 + }
1659 pushEffect(
1660 HookHasEffect | HookPassive,
1661 () => {
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+73 -9
@@ -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,
@@ -30,6 +31,7 @@ import {
31 enableScopeAPI,
32 skipUnmountedBoundaries,
33 disableSchedulerTimeoutInWorkLoop,
34 + enableDoubleInvokingEffects,
35 } from 'shared/ReactFeatureFlags';
36 import ReactSharedInternals from 'shared/ReactSharedInternals';
37 import invariant from 'shared/invariant';
@@ -136,6 +138,8 @@ import {
138 MutationMask,
139 LayoutMask,
140 PassiveMask,
141 + MountPassiveDev,
142 + MountLayoutDev,
143 } from './ReactFiberFlags';
144 import {
145 NoLanePriority,
@@ -198,6 +202,10 @@ import {
202 commitAttachRef,
203 commitResetTextContent,
204 isSuspenseBoundaryBeingHidden,
205 + invokeLayoutEffectMountInDEV,
206 + invokePassiveEffectMountInDEV,
207 + invokeLayoutEffectUnmountInDEV,
208 + invokePassiveEffectUnmountInDEV,
209 } from './ReactFiberCommitWork.new';
210 import {enqueueUpdate} from './ReactUpdateQueue.new';
211 import {resetContextDependencies} from './ReactFiberNewContext.new';
@@ -2027,6 +2035,12 @@ function commitRootImpl(root, renderPriorityLevel) {
2035 legacyErrorBoundariesThatAlreadyFailed = null;
2036 }
2037
2038 + if (__DEV__ && enableDoubleInvokingEffects) {
2039 + if (!rootDidHavePassiveEffects) {
2040 + commitDoubleInvokeEffectsInDEV(root.current, false);
2041 + }
2042 + }
2043 +
2044 if (enableSchedulerTracing) {
2045 if (!rootDidHavePassiveEffects) {
2046 // If there are no passive effects, then we can complete the pending interactions.
@@ -2590,15 +2604,6 @@ function flushPassiveEffectsImpl() {
2604 flushPassiveUnmountEffects(root.current);
2605 flushPassiveMountEffects(root, root.current);
2606
2593 - if (enableSchedulerTracing) {
2594 - popInteractions(((prevInteractions: any): Set<Interaction>));
2595 - finishPendingInteractions(root, lanes);
2596 - }
2597 -
2598 - if (__DEV__) {
2599 - isFlushingPassiveEffects = false;
2600 - }
2601 -
2607 if (__DEV__) {
2608 if (enableDebugTracing) {
2609 logPassiveEffectsStopped();
@@ -2609,6 +2614,19 @@ function flushPassiveEffectsImpl() {
2614 markPassiveEffectsStopped();
2615 }
2616
2617 + if (__DEV__ && enableDoubleInvokingEffects) {
2618 + commitDoubleInvokeEffectsInDEV(root.current, true);
2619 + }
2620 +
2621 + if (__DEV__) {
2622 + isFlushingPassiveEffects = false;
2623 + }
2624 +
2625 + if (enableSchedulerTracing) {
2626 + popInteractions(((prevInteractions: any): Set<Interaction>));
2627 + finishPendingInteractions(root, lanes);
2628 + }
2629 +
2630 executionContext = prevExecutionContext;
2631
2632 flushSyncCallbackQueue();
@@ -2886,6 +2904,52 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
2904 }
2905 }
2906
2907 +function commitDoubleInvokeEffectsInDEV(
2908 + fiber: Fiber,
2909 + hasPassiveEffects: boolean,
2910 +) {
2911 + if (__DEV__ && enableDoubleInvokingEffects) {
2912 + setCurrentDebugFiberInDEV(fiber);
2913 + invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
2914 + if (hasPassiveEffects) {
2915 + invokeEffectsInDev(
2916 + fiber,
2917 + MountPassiveDev,
2918 + invokePassiveEffectUnmountInDEV,
2919 + );
2920 + }
2921 +
2922 + invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
2923 + if (hasPassiveEffects) {
2924 + invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
2925 + }
2926 + resetCurrentDebugFiberInDEV();
2927 + }
2928 +}
2929 +
2930 +function invokeEffectsInDev(
2931 + firstChild: Fiber,
2932 + fiberFlags: Flags,
2933 + invokeEffectFn: (fiber: Fiber) => void,
2934 +): void {
2935 + if (__DEV__ && enableDoubleInvokingEffects) {
2936 + let fiber = firstChild;
2937 + while (fiber !== null) {
2938 + if (fiber.child !== null) {
2939 + const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags;
2940 + if (primarySubtreeFlag !== NoFlags) {
2941 + invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn);
2942 + }
2943 + }
2944 +
2945 + if ((fiber.flags & fiberFlags) !== NoFlags) {
2946 + invokeEffectFn(fiber);
2947 + }
2948 + fiber = fiber.sibling;
2949 + }
2950 + }
2951 +}
2952 +
2953 let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
2954 function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
2955 if (__DEV__) {
packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js new
+504
@@ -0,0 +1,504 @@
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 +describe('ReactDoubleInvokeEvents', () => {
18 + beforeEach(() => {
19 + jest.resetModules();
20 + React = require('react');
21 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
22 + ReactNoop = require('react-noop-renderer');
23 + Scheduler = require('scheduler');
24 + ReactFeatureFlags.enableDoubleInvokingEffects = __VARIANT__;
25 + });
26 +
27 + it('double invoking for effects works properly', () => {
28 + function App({text}) {
29 + React.useEffect(() => {
30 + Scheduler.unstable_yieldValue('useEffect mount');
31 + return () => Scheduler.unstable_yieldValue('useEffect unmount');
32 + });
33 +
34 + React.useLayoutEffect(() => {
35 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
36 + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
37 + });
38 +
39 + return text;
40 + }
41 + ReactNoop.act(() => {
42 + ReactNoop.render(<App text={'mount'} />);
43 + });
44 +
45 + if (__DEV__ && __VARIANT__) {
46 + expect(Scheduler).toHaveYielded([
47 + 'useLayoutEffect mount',
48 + 'useEffect mount',
49 + 'useLayoutEffect unmount',
50 + 'useEffect unmount',
51 + 'useLayoutEffect mount',
52 + 'useEffect mount',
53 + ]);
54 + } else {
55 + expect(Scheduler).toHaveYielded([
56 + 'useLayoutEffect mount',
57 + 'useEffect mount',
58 + ]);
59 + }
60 +
61 + ReactNoop.act(() => {
62 + ReactNoop.render(<App text={'update'} />);
63 + });
64 +
65 + expect(Scheduler).toHaveYielded([
66 + 'useLayoutEffect unmount',
67 + 'useLayoutEffect mount',
68 + 'useEffect unmount',
69 + 'useEffect mount',
70 + ]);
71 +
72 + ReactNoop.act(() => {
73 + ReactNoop.render(null);
74 + });
75 +
76 + expect(Scheduler).toHaveYielded([
77 + 'useLayoutEffect unmount',
78 + 'useEffect unmount',
79 + ]);
80 + });
81 +
82 + it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => {
83 + function App({text}) {
84 + React.useEffect(() => {
85 + Scheduler.unstable_yieldValue('useEffect One mount');
86 + return () => Scheduler.unstable_yieldValue('useEffect One unmount');
87 + });
88 +
89 + React.useEffect(() => {
90 + Scheduler.unstable_yieldValue('useEffect Two mount');
91 + return () => Scheduler.unstable_yieldValue('useEffect Two unmount');
92 + });
93 +
94 + return text;
95 + }
96 +
97 + ReactNoop.act(() => {
98 + ReactNoop.render(<App text={'mount'} />);
99 + });
100 +
101 + if (__DEV__ && __VARIANT__) {
102 + expect(Scheduler).toHaveYielded([
103 + 'useEffect One mount',
104 + 'useEffect Two mount',
105 + 'useEffect One unmount',
106 + 'useEffect Two unmount',
107 + 'useEffect One mount',
108 + 'useEffect Two mount',
109 + ]);
110 + } else {
111 + expect(Scheduler).toHaveYielded([
112 + 'useEffect One mount',
113 + 'useEffect Two mount',
114 + ]);
115 + }
116 +
117 + ReactNoop.act(() => {
118 + ReactNoop.render(<App text={'update'} />);
119 + });
120 +
121 + expect(Scheduler).toHaveYielded([
122 + 'useEffect One unmount',
123 + 'useEffect Two unmount',
124 + 'useEffect One mount',
125 + 'useEffect Two mount',
126 + ]);
127 +
128 + ReactNoop.act(() => {
129 + ReactNoop.render(null);
130 + });
131 +
132 + expect(Scheduler).toHaveYielded([
133 + 'useEffect One unmount',
134 + 'useEffect Two unmount',
135 + ]);
136 + });
137 +
138 + it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => {
139 + function App({text}) {
140 + React.useLayoutEffect(() => {
141 + Scheduler.unstable_yieldValue('useLayoutEffect One mount');
142 + return () =>
143 + Scheduler.unstable_yieldValue('useLayoutEffect One unmount');
144 + });
145 +
146 + React.useLayoutEffect(() => {
147 + Scheduler.unstable_yieldValue('useLayoutEffect Two mount');
148 + return () =>
149 + Scheduler.unstable_yieldValue('useLayoutEffect Two unmount');
150 + });
151 +
152 + return text;
153 + }
154 +
155 + ReactNoop.act(() => {
156 + ReactNoop.render(<App text={'mount'} />);
157 + });
158 +
159 + if (__DEV__ && __VARIANT__) {
160 + expect(Scheduler).toHaveYielded([
161 + 'useLayoutEffect One mount',
162 + 'useLayoutEffect Two mount',
163 + 'useLayoutEffect One unmount',
164 + 'useLayoutEffect Two unmount',
165 + 'useLayoutEffect One mount',
166 + 'useLayoutEffect Two mount',
167 + ]);
168 + } else {
169 + expect(Scheduler).toHaveYielded([
170 + 'useLayoutEffect One mount',
171 + 'useLayoutEffect Two mount',
172 + ]);
173 + }
174 +
175 + ReactNoop.act(() => {
176 + ReactNoop.render(<App text={'update'} />);
177 + });
178 +
179 + expect(Scheduler).toHaveYielded([
180 + 'useLayoutEffect One unmount',
181 + 'useLayoutEffect Two unmount',
182 + 'useLayoutEffect One mount',
183 + 'useLayoutEffect Two mount',
184 + ]);
185 +
186 + ReactNoop.act(() => {
187 + ReactNoop.render(null);
188 + });
189 +
190 + expect(Scheduler).toHaveYielded([
191 + 'useLayoutEffect One unmount',
192 + 'useLayoutEffect Two unmount',
193 + ]);
194 + });
195 +
196 + it('useEffect and useLayoutEffect is called twice when there is no unmount', () => {
197 + function App({text}) {
198 + React.useEffect(() => {
199 + Scheduler.unstable_yieldValue('useEffect mount');
200 + });
201 +
202 + React.useLayoutEffect(() => {
203 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
204 + });
205 +
206 + return text;
207 + }
208 +
209 + ReactNoop.act(() => {
210 + ReactNoop.render(<App text={'mount'} />);
211 + });
212 +
213 + if (__DEV__ && __VARIANT__) {
214 + expect(Scheduler).toHaveYielded([
215 + 'useLayoutEffect mount',
216 + 'useEffect mount',
217 + 'useLayoutEffect mount',
218 + 'useEffect mount',
219 + ]);
220 + } else {
221 + expect(Scheduler).toHaveYielded([
222 + 'useLayoutEffect mount',
223 + 'useEffect mount',
224 + ]);
225 + }
226 +
227 + ReactNoop.act(() => {
228 + ReactNoop.render(<App text={'update'} />);
229 + });
230 +
231 + expect(Scheduler).toHaveYielded([
232 + 'useLayoutEffect mount',
233 + 'useEffect mount',
234 + ]);
235 +
236 + ReactNoop.act(() => {
237 + ReactNoop.render(null);
238 + });
239 +
240 + expect(Scheduler).toHaveYielded([]);
241 + });
242 +
243 + it('double invoking works for class components', () => {
244 + class App extends React.PureComponent {
245 + componentDidMount() {
246 + Scheduler.unstable_yieldValue('componentDidMount');
247 + }
248 +
249 + componentDidUpdate() {
250 + Scheduler.unstable_yieldValue('componentDidUpdate');
251 + }
252 +
253 + componentWillUnmount() {
254 + Scheduler.unstable_yieldValue('componentWillUnmount');
255 + }
256 +
257 + render() {
258 + return this.props.text;
259 + }
260 + }
261 +
262 + ReactNoop.act(() => {
263 + ReactNoop.render(<App text={'mount'} />);
264 + });
265 +
266 + if (__DEV__ && __VARIANT__) {
267 + expect(Scheduler).toHaveYielded([
268 + 'componentDidMount',
269 + 'componentWillUnmount',
270 + 'componentDidMount',
271 + ]);
272 + } else {
273 + expect(Scheduler).toHaveYielded(['componentDidMount']);
274 + }
275 +
276 + ReactNoop.act(() => {
277 + ReactNoop.render(<App text={'update'} />);
278 + });
279 +
280 + expect(Scheduler).toHaveYielded(['componentDidUpdate']);
281 +
282 + ReactNoop.act(() => {
283 + ReactNoop.render(null);
284 + });
285 +
286 + expect(Scheduler).toHaveYielded(['componentWillUnmount']);
287 + });
288 +
289 + it('double flushing passive effects only results in one double invoke', () => {
290 + function App({text}) {
291 + const [state, setState] = React.useState(0);
292 + React.useEffect(() => {
293 + if (state !== 1) {
294 + setState(1);
295 + }
296 + Scheduler.unstable_yieldValue('useEffect mount');
297 + return () => Scheduler.unstable_yieldValue('useEffect unmount');
298 + });
299 +
300 + React.useLayoutEffect(() => {
301 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
302 + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
303 + });
304 +
305 + Scheduler.unstable_yieldValue(text);
306 + return text;
307 + }
308 +
309 + ReactNoop.act(() => {
310 + ReactNoop.render(<App text={'mount'} />);
311 + });
312 +
313 + if (__DEV__ && __VARIANT__) {
314 + expect(Scheduler).toHaveYielded([
315 + 'mount',
316 + 'useLayoutEffect mount',
317 + 'useEffect mount',
318 + 'useLayoutEffect unmount',
319 + 'useEffect unmount',
320 + 'useLayoutEffect mount',
321 + 'useEffect mount',
322 + 'mount',
323 + 'useLayoutEffect unmount',
324 + 'useLayoutEffect mount',
325 + 'useEffect unmount',
326 + 'useEffect mount',
327 + ]);
328 + } else {
329 + expect(Scheduler).toHaveYielded([
330 + 'mount',
331 + 'useLayoutEffect mount',
332 + 'useEffect mount',
333 + 'mount',
334 + 'useLayoutEffect unmount',
335 + 'useLayoutEffect mount',
336 + 'useEffect unmount',
337 + 'useEffect mount',
338 + ]);
339 + }
340 + });
341 +
342 + it('newly mounted components after initial mount get double invoked', () => {
343 + let _setShowChild;
344 + function Child() {
345 + React.useEffect(() => {
346 + Scheduler.unstable_yieldValue('Child useEffect mount');
347 + return () => Scheduler.unstable_yieldValue('Child useEffect unmount');
348 + });
349 + React.useLayoutEffect(() => {
350 + Scheduler.unstable_yieldValue('Child useLayoutEffect mount');
351 + return () =>
352 + Scheduler.unstable_yieldValue('Child useLayoutEffect unmount');
353 + });
354 +
355 + return null;
356 + }
357 +
358 + function App() {
359 + const [showChild, setShowChild] = React.useState(false);
360 + _setShowChild = setShowChild;
361 + React.useEffect(() => {
362 + Scheduler.unstable_yieldValue('App useEffect mount');
363 + return () => Scheduler.unstable_yieldValue('App useEffect unmount');
364 + });
365 + React.useLayoutEffect(() => {
366 + Scheduler.unstable_yieldValue('App useLayoutEffect mount');
367 + return () =>
368 + Scheduler.unstable_yieldValue('App useLayoutEffect unmount');
369 + });
370 +
371 + return showChild && <Child />;
372 + }
373 +
374 + ReactNoop.act(() => {
375 + ReactNoop.render(<App />);
376 + });
377 +
378 + if (__DEV__ && __VARIANT__) {
379 + expect(Scheduler).toHaveYielded([
380 + 'App useLayoutEffect mount',
381 + 'App useEffect mount',
382 + 'App useLayoutEffect unmount',
383 + 'App useEffect unmount',
384 + 'App useLayoutEffect mount',
385 + 'App useEffect mount',
386 + ]);
387 + } else {
388 + expect(Scheduler).toHaveYielded([
389 + 'App useLayoutEffect mount',
390 + 'App useEffect mount',
391 + ]);
392 + }
393 +
394 + ReactNoop.act(() => {
395 + _setShowChild(true);
396 + });
397 +
398 + if (__DEV__ && __VARIANT__) {
399 + expect(Scheduler).toHaveYielded([
400 + 'App useLayoutEffect unmount',
401 + 'Child useLayoutEffect mount',
402 + 'App useLayoutEffect mount',
403 + 'App useEffect unmount',
404 + 'Child useEffect mount',
405 + 'App useEffect mount',
406 + 'Child useLayoutEffect unmount',
407 + 'Child useEffect unmount',
408 + 'Child useLayoutEffect mount',
409 + 'Child useEffect mount',
410 + ]);
411 + } else {
412 + expect(Scheduler).toHaveYielded([
413 + 'App useLayoutEffect unmount',
414 + 'Child useLayoutEffect mount',
415 + 'App useLayoutEffect mount',
416 + 'App useEffect unmount',
417 + 'Child useEffect mount',
418 + 'App useEffect mount',
419 + ]);
420 + }
421 + });
422 +
423 + it('classes and functions are double invoked together correctly', () => {
424 + class ClassChild extends React.PureComponent {
425 + componentDidMount() {
426 + Scheduler.unstable_yieldValue('componentDidMount');
427 + }
428 +
429 + componentWillUnmount() {
430 + Scheduler.unstable_yieldValue('componentWillUnmount');
431 + }
432 +
433 + render() {
434 + return this.props.text;
435 + }
436 + }
437 +
438 + function FunctionChild({text}) {
439 + React.useEffect(() => {
440 + Scheduler.unstable_yieldValue('useEffect mount');
441 + return () => Scheduler.unstable_yieldValue('useEffect unmount');
442 + });
443 + React.useLayoutEffect(() => {
444 + Scheduler.unstable_yieldValue('useLayoutEffect mount');
445 + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
446 + });
447 + return text;
448 + }
449 +
450 + function App({text}) {
451 + return (
452 + <>
453 + <ClassChild text={text} />
454 + <FunctionChild text={text} />
455 + </>
456 + );
457 + }
458 +
459 + ReactNoop.act(() => {
460 + ReactNoop.render(<App text={'mount'} />);
461 + });
462 +
463 + if (__DEV__ && __VARIANT__) {
464 + expect(Scheduler).toHaveYielded([
465 + 'componentDidMount',
466 + 'useLayoutEffect mount',
467 + 'useEffect mount',
468 + 'componentWillUnmount',
469 + 'useLayoutEffect unmount',
470 + 'useEffect unmount',
471 + 'componentDidMount',
472 + 'useLayoutEffect mount',
473 + 'useEffect mount',
474 + ]);
475 + } else {
476 + expect(Scheduler).toHaveYielded([
477 + 'componentDidMount',
478 + 'useLayoutEffect mount',
479 + 'useEffect mount',
480 + ]);
481 + }
482 +
483 + ReactNoop.act(() => {
484 + ReactNoop.render(<App text={'mount'} />);
485 + });
486 +
487 + expect(Scheduler).toHaveYielded([
488 + 'useLayoutEffect unmount',
489 + 'useLayoutEffect mount',
490 + 'useEffect unmount',
491 + 'useEffect mount',
492 + ]);
493 +
494 + ReactNoop.act(() => {
495 + ReactNoop.render(null);
496 + });
497 +
498 + expect(Scheduler).toHaveYielded([
499 + 'componentWillUnmount',
500 + 'useLayoutEffect unmount',
501 + 'useEffect unmount',
502 + ]);
503 + });
504 +});
packages/react/src/__tests__/ReactProfiler-test.internal.js
+43
@@ -4149,6 +4149,49 @@ describe('Profiler', () => {
4149 expect(onRender.mock.calls[2][2]).toBe(15); // actual
4150 expect(onRender.mock.calls[2][3]).toBe(1 + 15); // base
4151 });
4152 +
4153 + if (__DEV__) {
4154 + // @gate new
4155 + it('double invoking does not disconnect wrapped async work', () => {
4156 + ReactFeatureFlags.enableDoubleInvokingEffects = true;
4157 +
4158 + const callback = jest.fn(() => {
4159 + const wrappedInteractions = SchedulerTracing.unstable_getCurrent();
4160 + // Expect wrappedInteractions and interactions to be the same set.
4161 + expect(wrappedInteractions).toMatchInteractions([interaction]);
4162 + });
4163 +
4164 + const Component = jest.fn(() => {
4165 + React.useEffect(() => {
4166 + setTimeout(SchedulerTracing.unstable_wrap(callback), 0);
4167 + });
4168 + React.useLayoutEffect(() => {
4169 + setTimeout(SchedulerTracing.unstable_wrap(callback), 0);
4170 + });
4171 +
4172 + return null;
4173 + });
4174 +
4175 + let interaction;
4176 + SchedulerTracing.unstable_trace(
4177 + 'event',
4178 + Scheduler.unstable_now(),
4179 + () => {
4180 + const interactions = SchedulerTracing.unstable_getCurrent();
4181 + expect(interactions.size).toBe(1);
4182 + interaction = Array.from(interactions)[0];
4183 + ReactTestRenderer.create(<Component />);
4184 + },
4185 + );
4186 + Scheduler.unstable_flushAll();
4187 +
4188 + jest.runAllTimers();
4189 +
4190 + expect(callback).toHaveBeenCalledTimes(4); // 2x per effect
4191 +
4192 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
4193 + });
4194 + }
4195 });
4196 });
4197 });
packages/shared/ReactFeatureFlags.js
+2
@@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false;
136 export const enableEagerRootListeners = true;
137
138 export const disableSchedulerTimeoutInWorkLoop = false;
139 +
140 +export const enableDoubleInvokingEffects = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+2
@@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
52 export const enableEagerRootListeners = true;
53 export const disableSchedulerTimeoutInWorkLoop = false;
54
55 +export const enableDoubleInvokingEffects = false;
56 +
57 // Flow magic to verify the exports of this file match the original version.
58 // eslint-disable-next-line no-unused-vars
59 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
51 export const enableEagerRootListeners = true;
52 export const disableSchedulerTimeoutInWorkLoop = false;
53
54 +export const enableDoubleInvokingEffects = false;
55 +
56 // Flow magic to verify the exports of this file match the original version.
57 // eslint-disable-next-line no-unused-vars
58 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+2
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
51 export const enableEagerRootListeners = true;
52 export const disableSchedulerTimeoutInWorkLoop = false;
53
54 +export const enableDoubleInvokingEffects = false;
55 +
56 // Flow magic to verify the exports of this file match the original version.
57 // eslint-disable-next-line no-unused-vars
58 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+2
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
51 export const enableEagerRootListeners = true;
52 export const disableSchedulerTimeoutInWorkLoop = false;
53
54 +export const enableDoubleInvokingEffects = false;
55 +
56 // Flow magic to verify the exports of this file match the original version.
57 // eslint-disable-next-line no-unused-vars
58 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
51 export const enableEagerRootListeners = true;
52 export const disableSchedulerTimeoutInWorkLoop = false;
53
54 +export const enableDoubleInvokingEffects = false;
55 +
56 // Flow magic to verify the exports of this file match the original version.
57 // eslint-disable-next-line no-unused-vars
58 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/ReactFeatureFlags.testing.js
+2
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
51 export const enableEagerRootListeners = true;
52 export const disableSchedulerTimeoutInWorkLoop = false;
53
54 +export const enableDoubleInvokingEffects = false;
55 +
56 // Flow magic to verify the exports of this file match the original version.
57 // eslint-disable-next-line no-unused-vars
58 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+2
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = true;
51 export const enableEagerRootListeners = true;
52 export const disableSchedulerTimeoutInWorkLoop = false;
53
54 +export const enableDoubleInvokingEffects = false;
55 +
56 // Flow magic to verify the exports of this file match the original version.
57 // eslint-disable-next-line no-unused-vars
58 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+2
@@ -48,3 +48,5 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
48 export const enableTrustedTypesIntegration = false;
49 export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
50 export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
51 +
52 +export const enableDoubleInvokingEffects = false;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -29,6 +29,7 @@ export const {
29 skipUnmountedBoundaries,
30 enableEagerRootListeners,
31 disableSchedulerTimeoutInWorkLoop,
32 + enableDoubleInvokingEffects,
33 } = dynamicFeatureFlags;
34
35 // On WWW, __EXPERIMENTAL__ is used for a new modern build.