Skip to content

Commit c63741f

Browse files
authored
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.**
1 parent a99bf5c commit c63741f

17 files changed

+888
-60
lines changed

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

+22-5
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

+135-5
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

+32-26
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;

0 commit comments

Comments
 (0)