Skip to content

Commit c63741f

Browse files
authoredSep 24, 2020
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.**
·
v19.1.10.14.10
1 parent a99bf5c commit c63741f

17 files changed

+888
-60
lines changed
 

‎packages/react-reconciler/src/ReactFiberClassComponent.new.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import type {Lanes} from './ReactFiberLane';
1212
import type {UpdateQueue} from './ReactUpdateQueue.new';
1313

1414
import * as React from 'react';
15-
import {Update, Snapshot} from './ReactFiberFlags';
15+
import {Update, Snapshot, MountLayoutDev} from './ReactFiberFlags';
1616
import {
1717
debugRenderPhaseSideEffectsForStrictMode,
1818
disableLegacyContext,
1919
enableDebugTracing,
2020
enableSchedulingProfiler,
2121
warnAboutDeprecatedLifecycles,
22+
enableDoubleInvokingEffects,
2223
} from 'shared/ReactFeatureFlags';
2324
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
2425
import {isMounted} from './ReactFiberTreeReflection';
@@ -890,7 +891,11 @@ function mountClassInstance(
890891
}
891892

892893
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+
}
894899
}
895900
}
896901

@@ -960,7 +965,11 @@ function resumeMountClassInstance(
960965
// If an update was already in progress, we should schedule an Update
961966
// effect even though we're bailing out, so that cWU/cDU are called.
962967
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+
}
964973
}
965974
return false;
966975
}
@@ -1003,13 +1012,21 @@ function resumeMountClassInstance(
10031012
}
10041013
}
10051014
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+
}
10071020
}
10081021
} else {
10091022
// If an update was already in progress, we should schedule an Update
10101023
// effect even though we're bailing out, so that cWU/cDU are called.
10111024
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+
}
10131030
}
10141031

10151032
// If shouldComponentUpdate returned false, we should still update the

‎packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
enableFundamentalAPI,
3636
enableSuspenseCallback,
3737
enableScopeAPI,
38+
enableDoubleInvokingEffects,
3839
} from 'shared/ReactFeatureFlags';
3940
import {
4041
FunctionComponent,
@@ -159,7 +160,7 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
159160
function safelyCallComponentWillUnmount(
160161
current: Fiber,
161162
instance: any,
162-
nearestMountedAncestor: Fiber,
163+
nearestMountedAncestor: Fiber | null,
163164
) {
164165
if (__DEV__) {
165166
invokeGuardedCallback(
@@ -318,7 +319,7 @@ function commitBeforeMutationLifeCycles(
318319
}
319320

320321
function commitHookEffectListUnmount(
321-
tag: HookFlags,
322+
flags: HookFlags,
322323
finishedWork: Fiber,
323324
nearestMountedAncestor: Fiber | null,
324325
) {
@@ -328,7 +329,7 @@ function commitHookEffectListUnmount(
328329
const firstEffect = lastEffect.next;
329330
let effect = firstEffect;
330331
do {
331-
if ((effect.tag & tag) === tag) {
332+
if ((effect.tag & flags) === flags) {
332333
// Unmount
333334
const destroy = effect.destroy;
334335
effect.destroy = undefined;
@@ -341,14 +342,14 @@ function commitHookEffectListUnmount(
341342
}
342343
}
343344

344-
function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
345+
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
345346
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
346347
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
347348
if (lastEffect !== null) {
348349
const firstEffect = lastEffect.next;
349350
let effect = firstEffect;
350351
do {
351-
if ((effect.tag & tag) === tag) {
352+
if ((effect.tag & flags) === flags) {
352353
// Mount
353354
const create = effect.create;
354355
effect.destroy = create();
@@ -1884,6 +1885,131 @@ function commitPassiveMount(
18841885
}
18851886
}
18861887

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+
18872013
export {
18882014
commitBeforeMutationLifeCycles,
18892015
commitResetTextContent,
@@ -1896,4 +2022,8 @@ export {
18962022
commitPassiveUnmount,
18972023
commitPassiveUnmountInsideDeletedTree,
18982024
commitPassiveMount,
2025+
invokeLayoutEffectMountInDEV,
2026+
invokeLayoutEffectUnmountInDEV,
2027+
invokePassiveEffectMountInDEV,
2028+
invokePassiveEffectUnmountInDEV,
18992029
};

‎packages/react-reconciler/src/ReactFiberFlags.js

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,50 +10,56 @@
1010
export type Flags = number;
1111

1212
// 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;
1515

1616
// 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;
2727
// 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;
3131

3232
// Passive & Update & Callback & Ref & Snapshot
33-
export const LifecycleEffectMask = /* */ 0b0000001110100100;
33+
export const LifecycleEffectMask = /* */ 0b000000001110100100;
3434

3535
// Union of all host effects
36-
export const HostEffectMask = /* */ 0b0000011111111111;
36+
export const HostEffectMask = /* */ 0b000000011111111111;
3737

3838
// 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;
4242

4343
// Static tags describe aspects of a fiber that are not specific to a render,
4444
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
4545
// This enables us to defer more work in the unmount case,
4646
// since we can defer traversing the tree during layout to look for Passive effects,
4747
// 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;
4949

5050
// 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;
5555

5656
// Union of tags that don't get reset on clones.
5757
// This allows certain concepts to persist without recalculting them,
5858
// 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

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
enableSchedulingProfiler,
2727
enableNewReconciler,
2828
decoupleUpdatePriorityFromScheduler,
29+
enableDoubleInvokingEffects,
2930
} from 'shared/ReactFeatureFlags';
3031

3132
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
@@ -48,6 +49,8 @@ import {
4849
Update as UpdateEffect,
4950
Passive as PassiveEffect,
5051
PassiveStatic as PassiveStaticEffect,
52+
MountLayoutDev as MountLayoutDevEffect,
53+
MountPassiveDev as MountPassiveDevEffect,
5154
} from './ReactFiberFlags';
5255
import {
5356
HasEffect as HookHasEffect,
@@ -482,7 +485,16 @@ export function bailoutHooks(
482485
lanes: Lanes,
483486
) {
484487
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+
}
486498
current.lanes = removeLanes(current.lanes, lanes);
487499
}
488500

@@ -1240,12 +1252,22 @@ function mountEffect(
12401252
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
12411253
}
12421254
}
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+
}
12491271
}
12501272

12511273
function updateEffect(
@@ -1265,7 +1287,16 @@ function mountLayoutEffect(
12651287
create: () => (() => void) | void,
12661288
deps: Array<mixed> | void | null,
12671289
): 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+
}
12691300
}
12701301

12711302
function updateLayoutEffect(
@@ -1324,12 +1355,21 @@ function mountImperativeHandle<T>(
13241355
const effectDeps =
13251356
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
13261357

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+
}
13331373
}
13341374

13351375
function updateImperativeHandle<T>(
@@ -1610,7 +1650,12 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
16101650
const setId = mountState(id)[1];
16111651

16121652
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+
}
16141659
pushEffect(
16151660
HookHasEffect | HookPassive,
16161661
() => {

‎packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {Interaction} from 'scheduler/src/Tracing';
1515
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
1616
import type {StackCursor} from './ReactFiberStack.new';
1717
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
18+
import type {Flags} from './ReactFiberFlags';
1819

1920
import {
2021
warnAboutDeprecatedLifecycles,
@@ -30,6 +31,7 @@ import {
3031
enableScopeAPI,
3132
skipUnmountedBoundaries,
3233
disableSchedulerTimeoutInWorkLoop,
34+
enableDoubleInvokingEffects,
3335
} from 'shared/ReactFeatureFlags';
3436
import ReactSharedInternals from 'shared/ReactSharedInternals';
3537
import invariant from 'shared/invariant';
@@ -136,6 +138,8 @@ import {
136138
MutationMask,
137139
LayoutMask,
138140
PassiveMask,
141+
MountPassiveDev,
142+
MountLayoutDev,
139143
} from './ReactFiberFlags';
140144
import {
141145
NoLanePriority,
@@ -198,6 +202,10 @@ import {
198202
commitAttachRef,
199203
commitResetTextContent,
200204
isSuspenseBoundaryBeingHidden,
205+
invokeLayoutEffectMountInDEV,
206+
invokePassiveEffectMountInDEV,
207+
invokeLayoutEffectUnmountInDEV,
208+
invokePassiveEffectUnmountInDEV,
201209
} from './ReactFiberCommitWork.new';
202210
import {enqueueUpdate} from './ReactUpdateQueue.new';
203211
import {resetContextDependencies} from './ReactFiberNewContext.new';
@@ -2027,6 +2035,12 @@ function commitRootImpl(root, renderPriorityLevel) {
20272035
legacyErrorBoundariesThatAlreadyFailed = null;
20282036
}
20292037

2038+
if (__DEV__ && enableDoubleInvokingEffects) {
2039+
if (!rootDidHavePassiveEffects) {
2040+
commitDoubleInvokeEffectsInDEV(root.current, false);
2041+
}
2042+
}
2043+
20302044
if (enableSchedulerTracing) {
20312045
if (!rootDidHavePassiveEffects) {
20322046
// If there are no passive effects, then we can complete the pending interactions.
@@ -2590,15 +2604,6 @@ function flushPassiveEffectsImpl() {
25902604
flushPassiveUnmountEffects(root.current);
25912605
flushPassiveMountEffects(root, root.current);
25922606

2593-
if (enableSchedulerTracing) {
2594-
popInteractions(((prevInteractions: any): Set<Interaction>));
2595-
finishPendingInteractions(root, lanes);
2596-
}
2597-
2598-
if (__DEV__) {
2599-
isFlushingPassiveEffects = false;
2600-
}
2601-
26022607
if (__DEV__) {
26032608
if (enableDebugTracing) {
26042609
logPassiveEffectsStopped();
@@ -2609,6 +2614,19 @@ function flushPassiveEffectsImpl() {
26092614
markPassiveEffectsStopped();
26102615
}
26112616

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+
26122630
executionContext = prevExecutionContext;
26132631

26142632
flushSyncCallbackQueue();
@@ -2886,6 +2904,52 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
28862904
}
28872905
}
28882906

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+
28892953
let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
28902954
function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
28912955
if (__DEV__) {

‎packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js

Lines changed: 504 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/react/src/__tests__/ReactProfiler-test.internal.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4149,6 +4149,49 @@ describe('Profiler', () => {
41494149
expect(onRender.mock.calls[2][2]).toBe(15); // actual
41504150
expect(onRender.mock.calls[2][3]).toBe(1 + 15); // base
41514151
});
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+
}
41524195
});
41534196
});
41544197
});

‎packages/shared/ReactFeatureFlags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false;
136136
export const enableEagerRootListeners = true;
137137

138138
export const disableSchedulerTimeoutInWorkLoop = false;
139+
140+
export const enableDoubleInvokingEffects = false;

‎packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
5252
export const enableEagerRootListeners = true;
5353
export const disableSchedulerTimeoutInWorkLoop = false;
5454

55+
export const enableDoubleInvokingEffects = false;
56+
5557
// Flow magic to verify the exports of this file match the original version.
5658
// eslint-disable-next-line no-unused-vars
5759
type Check<_X, Y: _X, X: Y = _X> = null;

‎packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
5151
export const enableEagerRootListeners = true;
5252
export const disableSchedulerTimeoutInWorkLoop = false;
5353

54+
export const enableDoubleInvokingEffects = false;
55+
5456
// Flow magic to verify the exports of this file match the original version.
5557
// eslint-disable-next-line no-unused-vars
5658
type Check<_X, Y: _X, X: Y = _X> = null;

‎packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
5151
export const enableEagerRootListeners = true;
5252
export const disableSchedulerTimeoutInWorkLoop = false;
5353

54+
export const enableDoubleInvokingEffects = false;
55+
5456
// Flow magic to verify the exports of this file match the original version.
5557
// eslint-disable-next-line no-unused-vars
5658
type Check<_X, Y: _X, X: Y = _X> = null;

‎packages/shared/forks/ReactFeatureFlags.test-renderer.native.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
5151
export const enableEagerRootListeners = true;
5252
export const disableSchedulerTimeoutInWorkLoop = false;
5353

54+
export const enableDoubleInvokingEffects = false;
55+
5456
// Flow magic to verify the exports of this file match the original version.
5557
// eslint-disable-next-line no-unused-vars
5658
type Check<_X, Y: _X, X: Y = _X> = null;

‎packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
5151
export const enableEagerRootListeners = true;
5252
export const disableSchedulerTimeoutInWorkLoop = false;
5353

54+
export const enableDoubleInvokingEffects = false;
55+
5456
// Flow magic to verify the exports of this file match the original version.
5557
// eslint-disable-next-line no-unused-vars
5658
type Check<_X, Y: _X, X: Y = _X> = null;

‎packages/shared/forks/ReactFeatureFlags.testing.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
5151
export const enableEagerRootListeners = true;
5252
export const disableSchedulerTimeoutInWorkLoop = false;
5353

54+
export const enableDoubleInvokingEffects = false;
55+
5456
// Flow magic to verify the exports of this file match the original version.
5557
// eslint-disable-next-line no-unused-vars
5658
type Check<_X, Y: _X, X: Y = _X> = null;

‎packages/shared/forks/ReactFeatureFlags.testing.www.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = true;
5151
export const enableEagerRootListeners = true;
5252
export const disableSchedulerTimeoutInWorkLoop = false;
5353

54+
export const enableDoubleInvokingEffects = false;
55+
5456
// Flow magic to verify the exports of this file match the original version.
5557
// eslint-disable-next-line no-unused-vars
5658
type Check<_X, Y: _X, X: Y = _X> = null;

‎packages/shared/forks/ReactFeatureFlags.www-dynamic.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,5 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
4848
export const enableTrustedTypesIntegration = false;
4949
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
5050
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
51+
52+
export const enableDoubleInvokingEffects = false;

‎packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const {
2929
skipUnmountedBoundaries,
3030
enableEagerRootListeners,
3131
disableSchedulerTimeoutInWorkLoop,
32+
enableDoubleInvokingEffects,
3233
} = dynamicFeatureFlags;
3334

3435
// On WWW, __EXPERIMENTAL__ is used for a new modern build.

0 commit comments

Comments
 (0)
Please sign in to comment.