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.**
This commit is contained in:
Luna Ruan
2020-09-24 13:42:17 -07:00
committed by GitHub
parent a99bf5c5f4
commit c63741fb3d
17 changed files with 888 additions and 60 deletions

View File

@@ -12,13 +12,14 @@ import type {Lanes} from './ReactFiberLane';
import type {UpdateQueue} from './ReactUpdateQueue.new';
import * as React from 'react';
import {Update, Snapshot} from './ReactFiberFlags';
import {Update, Snapshot, MountLayoutDev} from './ReactFiberFlags';
import {
debugRenderPhaseSideEffectsForStrictMode,
disableLegacyContext,
enableDebugTracing,
enableSchedulingProfiler,
warnAboutDeprecatedLifecycles,
enableDoubleInvokingEffects,
} from 'shared/ReactFeatureFlags';
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
import {isMounted} from './ReactFiberTreeReflection';
@@ -890,7 +891,11 @@ function mountClassInstance(
}
if (typeof instance.componentDidMount === 'function') {
workInProgress.flags |= Update;
if (__DEV__ && enableDoubleInvokingEffects) {
workInProgress.flags |= MountLayoutDev | Update;
} else {
workInProgress.flags |= Update;
}
}
}
@@ -960,7 +965,11 @@ function resumeMountClassInstance(
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidMount === 'function') {
workInProgress.flags |= Update;
if (__DEV__ && enableDoubleInvokingEffects) {
workInProgress.flags |= MountLayoutDev | Update;
} else {
workInProgress.flags |= Update;
}
}
return false;
}
@@ -1003,13 +1012,21 @@ function resumeMountClassInstance(
}
}
if (typeof instance.componentDidMount === 'function') {
workInProgress.flags |= Update;
if (__DEV__ && enableDoubleInvokingEffects) {
workInProgress.flags |= MountLayoutDev | Update;
} else {
workInProgress.flags |= Update;
}
}
} else {
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidMount === 'function') {
workInProgress.flags |= Update;
if (__DEV__ && enableDoubleInvokingEffects) {
workInProgress.flags |= MountLayoutDev | Update;
} else {
workInProgress.flags |= Update;
}
}
// If shouldComponentUpdate returned false, we should still update the

View File

@@ -35,6 +35,7 @@ import {
enableFundamentalAPI,
enableSuspenseCallback,
enableScopeAPI,
enableDoubleInvokingEffects,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -159,7 +160,7 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
function safelyCallComponentWillUnmount(
current: Fiber,
instance: any,
nearestMountedAncestor: Fiber,
nearestMountedAncestor: Fiber | null,
) {
if (__DEV__) {
invokeGuardedCallback(
@@ -318,7 +319,7 @@ function commitBeforeMutationLifeCycles(
}
function commitHookEffectListUnmount(
tag: HookFlags,
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
@@ -328,7 +329,7 @@ function commitHookEffectListUnmount(
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
@@ -341,14 +342,14 @@ function commitHookEffectListUnmount(
}
}
function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
effect.destroy = create();
@@ -1884,6 +1885,131 @@ function commitPassiveMount(
}
}
function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
if (__DEV__ && enableDoubleInvokingEffects) {
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
invokeGuardedCallback(
null,
commitHookEffectListMount,
null,
HookLayout | HookHasEffect,
fiber,
);
if (hasCaughtError()) {
const mountError = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, mountError);
}
break;
}
case ClassComponent: {
const instance = fiber.stateNode;
invokeGuardedCallback(null, instance.componentDidMount, null);
if (hasCaughtError()) {
const mountError = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, mountError);
}
break;
}
}
}
}
function invokePassiveEffectMountInDEV(fiber: Fiber): void {
if (__DEV__ && enableDoubleInvokingEffects) {
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
invokeGuardedCallback(
null,
commitHookEffectListMount,
null,
HookPassive | HookHasEffect,
fiber,
);
if (hasCaughtError()) {
const mountError = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, mountError);
}
break;
}
}
}
}
function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
if (__DEV__ && enableDoubleInvokingEffects) {
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
invokeGuardedCallback(
null,
commitHookEffectListUnmount,
null,
HookLayout | HookHasEffect,
fiber,
fiber.return,
);
if (hasCaughtError()) {
const unmountError = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, unmountError);
}
break;
}
case ClassComponent: {
const instance = fiber.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
invokeGuardedCallback(
null,
safelyCallComponentWillUnmount,
null,
fiber,
instance,
fiber.return,
);
if (hasCaughtError()) {
const unmountError = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, unmountError);
}
}
break;
}
}
}
}
function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
if (__DEV__ && enableDoubleInvokingEffects) {
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
invokeGuardedCallback(
null,
commitHookEffectListUnmount,
null,
HookPassive | HookHasEffect,
fiber,
fiber.return,
);
if (hasCaughtError()) {
const unmountError = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, unmountError);
}
break;
}
}
}
}
export {
commitBeforeMutationLifeCycles,
commitResetTextContent,
@@ -1896,4 +2022,8 @@ export {
commitPassiveUnmount,
commitPassiveUnmountInsideDeletedTree,
commitPassiveMount,
invokeLayoutEffectMountInDEV,
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectMountInDEV,
invokePassiveEffectUnmountInDEV,
};

View File

@@ -10,50 +10,56 @@
export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b0000000000000000;
export const PerformedWork = /* */ 0b0000000000000001;
export const NoFlags = /* */ 0b000000000000000000;
export const PerformedWork = /* */ 0b000000000000000001;
// You can change the rest (and add more).
export const Placement = /* */ 0b0000000000000010;
export const Update = /* */ 0b0000000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000000110;
export const Deletion = /* */ 0b0000000000001000;
export const ContentReset = /* */ 0b0000000000010000;
export const Callback = /* */ 0b0000000000100000;
export const DidCapture = /* */ 0b0000000001000000;
export const Ref = /* */ 0b0000000010000000;
export const Snapshot = /* */ 0b0000000100000000;
export const Passive = /* */ 0b0000001000000000;
export const Placement = /* */ 0b000000000000000010;
export const Update = /* */ 0b000000000000000100;
export const PlacementAndUpdate = /* */ 0b000000000000000110;
export const Deletion = /* */ 0b000000000000001000;
export const ContentReset = /* */ 0b000000000000010000;
export const Callback = /* */ 0b000000000000100000;
export const DidCapture = /* */ 0b000000000001000000;
export const Ref = /* */ 0b000000000010000000;
export const Snapshot = /* */ 0b000000000100000000;
export const Passive = /* */ 0b000000001000000000;
// TODO (effects) Remove this bit once the new reconciler is synced to the old.
export const PassiveUnmountPendingDev = /* */ 0b0010000000000000;
export const Hydrating = /* */ 0b0000010000000000;
export const HydratingAndUpdate = /* */ 0b0000010000000100;
export const PassiveUnmountPendingDev = /* */ 0b000010000000000000;
export const Hydrating = /* */ 0b000000010000000000;
export const HydratingAndUpdate = /* */ 0b000000010000000100;
// Passive & Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /* */ 0b0000001110100100;
export const LifecycleEffectMask = /* */ 0b000000001110100100;
// Union of all host effects
export const HostEffectMask = /* */ 0b0000011111111111;
export const HostEffectMask = /* */ 0b000000011111111111;
// These are not really side effects, but we still reuse this field.
export const Incomplete = /* */ 0b0000100000000000;
export const ShouldCapture = /* */ 0b0001000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b0100000000000000;
export const Incomplete = /* */ 0b000000100000000000;
export const ShouldCapture = /* */ 0b000001000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b000100000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const PassiveStatic = /* */ 0b1000000000000000;
export const PassiveStatic = /* */ 0b001000000000000000;
// Union of side effect groupings as pertains to subtreeFlags
export const BeforeMutationMask = /* */ 0b0000001100001010;
export const MutationMask = /* */ 0b0000010010011110;
export const LayoutMask = /* */ 0b0000000010100100;
export const PassiveMask = /* */ 0b0000001000001000;
export const BeforeMutationMask = /* */ 0b000000001100001010;
export const MutationMask = /* */ 0b000000010010011110;
export const LayoutMask = /* */ 0b000000000010100100;
export const PassiveMask = /* */ 0b000000001000001000;
// Union of tags that don't get reset on clones.
// This allows certain concepts to persist without recalculting them,
// e.g. whether a subtree contains passive effects or portals.
export const StaticMask = /* */ 0b1000000000000000;
export const StaticMask = /* */ 0b001000000000000000;
// These flags allow us to traverse to fibers that have effects on mount
// without traversing the entire tree after every commit for
// double invoking
export const MountLayoutDev = /* */ 0b010000000000000000;
export const MountPassiveDev = /* */ 0b100000000000000000;

View File

@@ -26,6 +26,7 @@ import {
enableSchedulingProfiler,
enableNewReconciler,
decoupleUpdatePriorityFromScheduler,
enableDoubleInvokingEffects,
} from 'shared/ReactFeatureFlags';
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
@@ -48,6 +49,8 @@ import {
Update as UpdateEffect,
Passive as PassiveEffect,
PassiveStatic as PassiveStaticEffect,
MountLayoutDev as MountLayoutDevEffect,
MountPassiveDev as MountPassiveDevEffect,
} from './ReactFiberFlags';
import {
HasEffect as HookHasEffect,
@@ -482,7 +485,16 @@ export function bailoutHooks(
lanes: Lanes,
) {
workInProgress.updateQueue = current.updateQueue;
workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
if (__DEV__ && enableDoubleInvokingEffects) {
workInProgress.flags &= ~(
MountPassiveDevEffect |
PassiveEffect |
MountLayoutDevEffect |
UpdateEffect
);
} else {
workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
}
current.lanes = removeLanes(current.lanes, lanes);
}
@@ -1240,12 +1252,22 @@ function mountEffect(
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
}
}
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
if (__DEV__ && enableDoubleInvokingEffects) {
return mountEffectImpl(
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
} else {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
}
function updateEffect(
@@ -1265,7 +1287,16 @@ function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
if (__DEV__ && enableDoubleInvokingEffects) {
return mountEffectImpl(
MountLayoutDevEffect | UpdateEffect,
HookLayout,
create,
deps,
);
} else {
return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
}
}
function updateLayoutEffect(
@@ -1324,12 +1355,21 @@ function mountImperativeHandle<T>(
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return mountEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
if (__DEV__ && enableDoubleInvokingEffects) {
return mountEffectImpl(
MountLayoutDevEffect | UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
} else {
return mountEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
}
function updateImperativeHandle<T>(
@@ -1610,7 +1650,12 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
const setId = mountState(id)[1];
if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
currentlyRenderingFiber.flags |= PassiveEffect | PassiveStaticEffect;
if (__DEV__ && enableDoubleInvokingEffects) {
currentlyRenderingFiber.flags |=
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect;
} else {
currentlyRenderingFiber.flags |= PassiveEffect | PassiveStaticEffect;
}
pushEffect(
HookHasEffect | HookPassive,
() => {

View File

@@ -15,6 +15,7 @@ import type {Interaction} from 'scheduler/src/Tracing';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {StackCursor} from './ReactFiberStack.new';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
import type {Flags} from './ReactFiberFlags';
import {
warnAboutDeprecatedLifecycles,
@@ -30,6 +31,7 @@ import {
enableScopeAPI,
skipUnmountedBoundaries,
disableSchedulerTimeoutInWorkLoop,
enableDoubleInvokingEffects,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
@@ -136,6 +138,8 @@ import {
MutationMask,
LayoutMask,
PassiveMask,
MountPassiveDev,
MountLayoutDev,
} from './ReactFiberFlags';
import {
NoLanePriority,
@@ -198,6 +202,10 @@ import {
commitAttachRef,
commitResetTextContent,
isSuspenseBoundaryBeingHidden,
invokeLayoutEffectMountInDEV,
invokePassiveEffectMountInDEV,
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectUnmountInDEV,
} from './ReactFiberCommitWork.new';
import {enqueueUpdate} from './ReactUpdateQueue.new';
import {resetContextDependencies} from './ReactFiberNewContext.new';
@@ -2027,6 +2035,12 @@ function commitRootImpl(root, renderPriorityLevel) {
legacyErrorBoundariesThatAlreadyFailed = null;
}
if (__DEV__ && enableDoubleInvokingEffects) {
if (!rootDidHavePassiveEffects) {
commitDoubleInvokeEffectsInDEV(root.current, false);
}
}
if (enableSchedulerTracing) {
if (!rootDidHavePassiveEffects) {
// If there are no passive effects, then we can complete the pending interactions.
@@ -2590,15 +2604,6 @@ function flushPassiveEffectsImpl() {
flushPassiveUnmountEffects(root.current);
flushPassiveMountEffects(root, root.current);
if (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
finishPendingInteractions(root, lanes);
}
if (__DEV__) {
isFlushingPassiveEffects = false;
}
if (__DEV__) {
if (enableDebugTracing) {
logPassiveEffectsStopped();
@@ -2609,6 +2614,19 @@ function flushPassiveEffectsImpl() {
markPassiveEffectsStopped();
}
if (__DEV__ && enableDoubleInvokingEffects) {
commitDoubleInvokeEffectsInDEV(root.current, true);
}
if (__DEV__) {
isFlushingPassiveEffects = false;
}
if (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
finishPendingInteractions(root, lanes);
}
executionContext = prevExecutionContext;
flushSyncCallbackQueue();
@@ -2886,6 +2904,52 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
}
}
function commitDoubleInvokeEffectsInDEV(
fiber: Fiber,
hasPassiveEffects: boolean,
) {
if (__DEV__ && enableDoubleInvokingEffects) {
setCurrentDebugFiberInDEV(fiber);
invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
if (hasPassiveEffects) {
invokeEffectsInDev(
fiber,
MountPassiveDev,
invokePassiveEffectUnmountInDEV,
);
}
invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
if (hasPassiveEffects) {
invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
}
resetCurrentDebugFiberInDEV();
}
}
function invokeEffectsInDev(
firstChild: Fiber,
fiberFlags: Flags,
invokeEffectFn: (fiber: Fiber) => void,
): void {
if (__DEV__ && enableDoubleInvokingEffects) {
let fiber = firstChild;
while (fiber !== null) {
if (fiber.child !== null) {
const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags;
if (primarySubtreeFlag !== NoFlags) {
invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn);
}
}
if ((fiber.flags & fiberFlags) !== NoFlags) {
invokeEffectFn(fiber);
}
fiber = fiber.sibling;
}
}
}
let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
if (__DEV__) {

View File

@@ -0,0 +1,504 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactFeatureFlags;
let ReactNoop;
let Scheduler;
describe('ReactDoubleInvokeEvents', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
ReactFeatureFlags.enableDoubleInvokingEffects = __VARIANT__;
});
it('double invoking for effects works properly', () => {
function App({text}) {
React.useEffect(() => {
Scheduler.unstable_yieldValue('useEffect mount');
return () => Scheduler.unstable_yieldValue('useEffect unmount');
});
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('useLayoutEffect mount');
return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
});
return text;
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'useLayoutEffect mount',
'useEffect mount',
'useLayoutEffect unmount',
'useEffect unmount',
'useLayoutEffect mount',
'useEffect mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'useLayoutEffect mount',
'useEffect mount',
]);
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'update'} />);
});
expect(Scheduler).toHaveYielded([
'useLayoutEffect unmount',
'useLayoutEffect mount',
'useEffect unmount',
'useEffect mount',
]);
ReactNoop.act(() => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
'useLayoutEffect unmount',
'useEffect unmount',
]);
});
it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => {
function App({text}) {
React.useEffect(() => {
Scheduler.unstable_yieldValue('useEffect One mount');
return () => Scheduler.unstable_yieldValue('useEffect One unmount');
});
React.useEffect(() => {
Scheduler.unstable_yieldValue('useEffect Two mount');
return () => Scheduler.unstable_yieldValue('useEffect Two unmount');
});
return text;
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'useEffect One mount',
'useEffect Two mount',
'useEffect One unmount',
'useEffect Two unmount',
'useEffect One mount',
'useEffect Two mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'useEffect One mount',
'useEffect Two mount',
]);
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'update'} />);
});
expect(Scheduler).toHaveYielded([
'useEffect One unmount',
'useEffect Two unmount',
'useEffect One mount',
'useEffect Two mount',
]);
ReactNoop.act(() => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
'useEffect One unmount',
'useEffect Two unmount',
]);
});
it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => {
function App({text}) {
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('useLayoutEffect One mount');
return () =>
Scheduler.unstable_yieldValue('useLayoutEffect One unmount');
});
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('useLayoutEffect Two mount');
return () =>
Scheduler.unstable_yieldValue('useLayoutEffect Two unmount');
});
return text;
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'useLayoutEffect One mount',
'useLayoutEffect Two mount',
'useLayoutEffect One unmount',
'useLayoutEffect Two unmount',
'useLayoutEffect One mount',
'useLayoutEffect Two mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'useLayoutEffect One mount',
'useLayoutEffect Two mount',
]);
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'update'} />);
});
expect(Scheduler).toHaveYielded([
'useLayoutEffect One unmount',
'useLayoutEffect Two unmount',
'useLayoutEffect One mount',
'useLayoutEffect Two mount',
]);
ReactNoop.act(() => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
'useLayoutEffect One unmount',
'useLayoutEffect Two unmount',
]);
});
it('useEffect and useLayoutEffect is called twice when there is no unmount', () => {
function App({text}) {
React.useEffect(() => {
Scheduler.unstable_yieldValue('useEffect mount');
});
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('useLayoutEffect mount');
});
return text;
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'useLayoutEffect mount',
'useEffect mount',
'useLayoutEffect mount',
'useEffect mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'useLayoutEffect mount',
'useEffect mount',
]);
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'update'} />);
});
expect(Scheduler).toHaveYielded([
'useLayoutEffect mount',
'useEffect mount',
]);
ReactNoop.act(() => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([]);
});
it('double invoking works for class components', () => {
class App extends React.PureComponent {
componentDidMount() {
Scheduler.unstable_yieldValue('componentDidMount');
}
componentDidUpdate() {
Scheduler.unstable_yieldValue('componentDidUpdate');
}
componentWillUnmount() {
Scheduler.unstable_yieldValue('componentWillUnmount');
}
render() {
return this.props.text;
}
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'componentDidMount',
'componentWillUnmount',
'componentDidMount',
]);
} else {
expect(Scheduler).toHaveYielded(['componentDidMount']);
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'update'} />);
});
expect(Scheduler).toHaveYielded(['componentDidUpdate']);
ReactNoop.act(() => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded(['componentWillUnmount']);
});
it('double flushing passive effects only results in one double invoke', () => {
function App({text}) {
const [state, setState] = React.useState(0);
React.useEffect(() => {
if (state !== 1) {
setState(1);
}
Scheduler.unstable_yieldValue('useEffect mount');
return () => Scheduler.unstable_yieldValue('useEffect unmount');
});
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('useLayoutEffect mount');
return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
});
Scheduler.unstable_yieldValue(text);
return text;
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'mount',
'useLayoutEffect mount',
'useEffect mount',
'useLayoutEffect unmount',
'useEffect unmount',
'useLayoutEffect mount',
'useEffect mount',
'mount',
'useLayoutEffect unmount',
'useLayoutEffect mount',
'useEffect unmount',
'useEffect mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'mount',
'useLayoutEffect mount',
'useEffect mount',
'mount',
'useLayoutEffect unmount',
'useLayoutEffect mount',
'useEffect unmount',
'useEffect mount',
]);
}
});
it('newly mounted components after initial mount get double invoked', () => {
let _setShowChild;
function Child() {
React.useEffect(() => {
Scheduler.unstable_yieldValue('Child useEffect mount');
return () => Scheduler.unstable_yieldValue('Child useEffect unmount');
});
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('Child useLayoutEffect mount');
return () =>
Scheduler.unstable_yieldValue('Child useLayoutEffect unmount');
});
return null;
}
function App() {
const [showChild, setShowChild] = React.useState(false);
_setShowChild = setShowChild;
React.useEffect(() => {
Scheduler.unstable_yieldValue('App useEffect mount');
return () => Scheduler.unstable_yieldValue('App useEffect unmount');
});
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('App useLayoutEffect mount');
return () =>
Scheduler.unstable_yieldValue('App useLayoutEffect unmount');
});
return showChild && <Child />;
}
ReactNoop.act(() => {
ReactNoop.render(<App />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'App useLayoutEffect mount',
'App useEffect mount',
'App useLayoutEffect unmount',
'App useEffect unmount',
'App useLayoutEffect mount',
'App useEffect mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'App useLayoutEffect mount',
'App useEffect mount',
]);
}
ReactNoop.act(() => {
_setShowChild(true);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'App useLayoutEffect unmount',
'Child useLayoutEffect mount',
'App useLayoutEffect mount',
'App useEffect unmount',
'Child useEffect mount',
'App useEffect mount',
'Child useLayoutEffect unmount',
'Child useEffect unmount',
'Child useLayoutEffect mount',
'Child useEffect mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'App useLayoutEffect unmount',
'Child useLayoutEffect mount',
'App useLayoutEffect mount',
'App useEffect unmount',
'Child useEffect mount',
'App useEffect mount',
]);
}
});
it('classes and functions are double invoked together correctly', () => {
class ClassChild extends React.PureComponent {
componentDidMount() {
Scheduler.unstable_yieldValue('componentDidMount');
}
componentWillUnmount() {
Scheduler.unstable_yieldValue('componentWillUnmount');
}
render() {
return this.props.text;
}
}
function FunctionChild({text}) {
React.useEffect(() => {
Scheduler.unstable_yieldValue('useEffect mount');
return () => Scheduler.unstable_yieldValue('useEffect unmount');
});
React.useLayoutEffect(() => {
Scheduler.unstable_yieldValue('useLayoutEffect mount');
return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount');
});
return text;
}
function App({text}) {
return (
<>
<ClassChild text={text} />
<FunctionChild text={text} />
</>
);
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
if (__DEV__ && __VARIANT__) {
expect(Scheduler).toHaveYielded([
'componentDidMount',
'useLayoutEffect mount',
'useEffect mount',
'componentWillUnmount',
'useLayoutEffect unmount',
'useEffect unmount',
'componentDidMount',
'useLayoutEffect mount',
'useEffect mount',
]);
} else {
expect(Scheduler).toHaveYielded([
'componentDidMount',
'useLayoutEffect mount',
'useEffect mount',
]);
}
ReactNoop.act(() => {
ReactNoop.render(<App text={'mount'} />);
});
expect(Scheduler).toHaveYielded([
'useLayoutEffect unmount',
'useLayoutEffect mount',
'useEffect unmount',
'useEffect mount',
]);
ReactNoop.act(() => {
ReactNoop.render(null);
});
expect(Scheduler).toHaveYielded([
'componentWillUnmount',
'useLayoutEffect unmount',
'useEffect unmount',
]);
});
});

View File

@@ -4149,6 +4149,49 @@ describe('Profiler', () => {
expect(onRender.mock.calls[2][2]).toBe(15); // actual
expect(onRender.mock.calls[2][3]).toBe(1 + 15); // base
});
if (__DEV__) {
// @gate new
it('double invoking does not disconnect wrapped async work', () => {
ReactFeatureFlags.enableDoubleInvokingEffects = true;
const callback = jest.fn(() => {
const wrappedInteractions = SchedulerTracing.unstable_getCurrent();
// Expect wrappedInteractions and interactions to be the same set.
expect(wrappedInteractions).toMatchInteractions([interaction]);
});
const Component = jest.fn(() => {
React.useEffect(() => {
setTimeout(SchedulerTracing.unstable_wrap(callback), 0);
});
React.useLayoutEffect(() => {
setTimeout(SchedulerTracing.unstable_wrap(callback), 0);
});
return null;
});
let interaction;
SchedulerTracing.unstable_trace(
'event',
Scheduler.unstable_now(),
() => {
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
interaction = Array.from(interactions)[0];
ReactTestRenderer.create(<Component />);
},
);
Scheduler.unstable_flushAll();
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(4); // 2x per effect
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
});
}
});
});
});

View File

@@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;

View File

@@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -51,6 +51,8 @@ export const enableDiscreteEventFlushingChange = true;
export const enableEagerRootListeners = true;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableDoubleInvokingEffects = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -48,3 +48,5 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
export const enableTrustedTypesIntegration = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
export const enableDoubleInvokingEffects = false;

View File

@@ -29,6 +29,7 @@ export const {
skipUnmountedBoundaries,
enableEagerRootListeners,
disableSchedulerTimeoutInWorkLoop,
enableDoubleInvokingEffects,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.