diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 77b074a17522..f4282ac4fa5b 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -26,6 +26,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { + deferPassiveEffectCleanupDuringUnmount, enableSchedulerTracing, enableProfilerTimer, enableSuspenseServerRenderer, @@ -109,16 +110,13 @@ import { captureCommitPhaseError, resolveRetryThenable, markCommitTimeOfFallback, + enqueuePendingPassiveEffectDestroyFn, } from './ReactFiberWorkLoop'; import { NoEffect as NoHookEffect, - UnmountSnapshot, - UnmountMutation, - MountMutation, - UnmountLayout, - MountLayout, - UnmountPassive, - MountPassive, + HasEffect as HookHasEffect, + Layout as HookLayout, + Passive as HookPassive, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork'; import {runWithPriority, NormalPriority} from './SchedulerWithReactIntegration'; @@ -250,7 +248,6 @@ function commitBeforeMutationLifeCycles( case ForwardRef: case SimpleMemoComponent: case Chunk: { - commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork); return; } case ClassComponent: { @@ -328,18 +325,14 @@ function commitBeforeMutationLifeCycles( ); } -function commitHookEffectList( - unmountTag: number, - mountTag: number, - finishedWork: Fiber, -) { +function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { - if ((effect.tag & unmountTag) !== NoHookEffect) { + if ((effect.tag & tag) === tag) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; @@ -347,7 +340,19 @@ function commitHookEffectList( destroy(); } } - if ((effect.tag & mountTag) !== NoHookEffect) { + effect = effect.next; + } while (effect !== firstEffect); + } +} + +function commitHookEffectListMount(tag: number, finishedWork: Fiber) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + if ((effect.tag & tag) === tag) { // Mount const create = effect.create; effect.destroy = create(); @@ -398,8 +403,11 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void { case ForwardRef: case SimpleMemoComponent: case Chunk: { - commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); - commitHookEffectList(NoHookEffect, MountPassive, finishedWork); + // TODO (#17945) We should call all passive destroy functions (for all fibers) + // before calling any create functions. The current approach only serializes + // these for a single fiber. + commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork); + commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); break; } default: @@ -419,7 +427,11 @@ function commitLifeCycles( case ForwardRef: case SimpleMemoComponent: case Chunk: { - commitHookEffectList(UnmountLayout, MountLayout, finishedWork); + // At this point layout effects have already been destroyed (during mutation phase). + // This is done to prevent sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); return; } case ClassComponent: { @@ -756,32 +768,47 @@ function commitUnmount( if (lastEffect !== null) { const firstEffect = lastEffect.next; - // When the owner fiber is deleted, the destroy function of a passive - // effect hook is called during the synchronous commit phase. This is - // a concession to implementation complexity. Calling it in the - // passive effect phase (like they usually are, when dependencies - // change during an update) would require either traversing the - // children of the deleted fiber again, or including unmount effects - // as part of the fiber effect list. - // - // Because this is during the sync commit phase, we need to change - // the priority. - // - // TODO: Reconsider this implementation trade off. - const priorityLevel = - renderPriorityLevel > NormalPriority - ? NormalPriority - : renderPriorityLevel; - runWithPriority(priorityLevel, () => { + if (deferPassiveEffectCleanupDuringUnmount) { let effect = firstEffect; do { - const destroy = effect.destroy; + const {destroy, tag} = effect; if (destroy !== undefined) { - safelyCallDestroy(current, destroy); + if ((tag & HookPassive) !== NoHookEffect) { + enqueuePendingPassiveEffectDestroyFn(destroy); + } else { + safelyCallDestroy(current, destroy); + } } effect = effect.next; } while (effect !== firstEffect); - }); + } else { + // When the owner fiber is deleted, the destroy function of a passive + // effect hook is called during the synchronous commit phase. This is + // a concession to implementation complexity. Calling it in the + // passive effect phase (like they usually are, when dependencies + // change during an update) would require either traversing the + // children of the deleted fiber again, or including unmount effects + // as part of the fiber effect list. + // + // Because this is during the sync commit phase, we need to change + // the priority. + // + // TODO: Reconsider this implementation trade off. + const priorityLevel = + renderPriorityLevel > NormalPriority + ? NormalPriority + : renderPriorityLevel; + runWithPriority(priorityLevel, () => { + let effect = firstEffect; + do { + const destroy = effect.destroy; + if (destroy !== undefined) { + safelyCallDestroy(current, destroy); + } + effect = effect.next; + } while (effect !== firstEffect); + }); + } } } return; @@ -1285,9 +1312,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { case MemoComponent: case SimpleMemoComponent: case Chunk: { - // Note: We currently never use MountMutation, but useLayout uses - // UnmountMutation. - commitHookEffectList(UnmountMutation, MountMutation, finishedWork); + // Layout effects are destroyed during the mutation phase so that all + // destroy functions for all fibers are called before any create functions. + // This prevents sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); return; } case Profiler: { @@ -1325,9 +1355,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { case MemoComponent: case SimpleMemoComponent: case Chunk: { - // Note: We currently never use MountMutation, but useLayout uses - // UnmountMutation. - commitHookEffectList(UnmountMutation, MountMutation, finishedWork); + // Layout effects are destroyed during the mutation phase so that all + // destroy functions for all fibers are called before any create functions. + // This prevents sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); return; } case ClassComponent: { diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 02191ffe5d90..fdb0875f8bb5 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -28,11 +28,9 @@ import { Passive as PassiveEffect, } from 'shared/ReactSideEffectTags'; import { - NoEffect as NoHookEffect, - UnmountMutation, - MountLayout, - UnmountPassive, - MountPassive, + HasEffect as HookHasEffect, + Layout as HookLayout, + Passive as HookPassive, } from './ReactHookEffectTags'; import { scheduleWork, @@ -923,7 +921,12 @@ function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.effectTag |= fiberEffectTag; - hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps); + hook.memoizedState = pushEffect( + HookHasEffect | hookEffectTag, + create, + undefined, + nextDeps, + ); } function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { @@ -937,7 +940,7 @@ function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { if (nextDeps !== null) { const prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) { - pushEffect(NoHookEffect, create, destroy, nextDeps); + pushEffect(hookEffectTag, create, destroy, nextDeps); return; } } @@ -945,7 +948,12 @@ function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { currentlyRenderingFiber.effectTag |= fiberEffectTag; - hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps); + hook.memoizedState = pushEffect( + HookHasEffect | hookEffectTag, + create, + destroy, + nextDeps, + ); } function mountEffect( @@ -960,7 +968,7 @@ function mountEffect( } return mountEffectImpl( UpdateEffect | PassiveEffect, - UnmountPassive | MountPassive, + HookPassive, create, deps, ); @@ -978,7 +986,7 @@ function updateEffect( } return updateEffectImpl( UpdateEffect | PassiveEffect, - UnmountPassive | MountPassive, + HookPassive, create, deps, ); @@ -988,24 +996,14 @@ function mountLayoutEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - return mountEffectImpl( - UpdateEffect, - UnmountMutation | MountLayout, - create, - deps, - ); + return mountEffectImpl(UpdateEffect, HookLayout, create, deps); } function updateLayoutEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - return updateEffectImpl( - UpdateEffect, - UnmountMutation | MountLayout, - create, - deps, - ); + return updateEffectImpl(UpdateEffect, HookLayout, create, deps); } function imperativeHandleEffect( @@ -1059,7 +1057,7 @@ function mountImperativeHandle( return mountEffectImpl( UpdateEffect, - UnmountMutation | MountLayout, + HookLayout, imperativeHandleEffect.bind(null, create, ref), effectDeps, ); @@ -1086,7 +1084,7 @@ function updateImperativeHandle( return updateEffectImpl( UpdateEffect, - UnmountMutation | MountLayout, + HookLayout, imperativeHandleEffect.bind(null, create, ref), effectDeps, ); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index a4664613ac22..4c067de698ec 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -18,6 +18,7 @@ import type {Hook} from './ReactFiberHooks'; import { warnAboutDeprecatedLifecycles, + deferPassiveEffectCleanupDuringUnmount, enableUserTimingAPI, enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, @@ -257,6 +258,7 @@ let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority; let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork; +let pendingUnmountedPassiveEffectDestroyFunctions: Array<() => void> = []; let rootsWithPendingDiscreteUpdates: Map< FiberRoot, @@ -2176,6 +2178,21 @@ export function flushPassiveEffects() { } } +export function enqueuePendingPassiveEffectDestroyFn( + destroy: () => void, +): void { + if (deferPassiveEffectCleanupDuringUnmount) { + pendingUnmountedPassiveEffectDestroyFunctions.push(destroy); + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalPriority, () => { + flushPassiveEffects(); + return null; + }); + } + } +} + function flushPassiveEffectsImpl() { if (rootWithPendingPassiveEffects === null) { return false; @@ -2193,6 +2210,20 @@ function flushPassiveEffectsImpl() { executionContext |= CommitContext; const prevInteractions = pushInteractions(root); + if (deferPassiveEffectCleanupDuringUnmount) { + // Flush any pending passive effect destroy functions that belong to + // components that were unmounted during the most recent commit. + for ( + let i = 0; + i < pendingUnmountedPassiveEffectDestroyFunctions.length; + i++ + ) { + const destroy = pendingUnmountedPassiveEffectDestroyFunctions[i]; + invokeGuardedCallback(null, destroy, null); + } + pendingUnmountedPassiveEffectDestroyFunctions.length = 0; + } + // Note: This currently assumes there are no passive effects on the root // fiber, because the root is not part of its own effect list. This could // change in the future. diff --git a/packages/react-reconciler/src/ReactHookEffectTags.js b/packages/react-reconciler/src/ReactHookEffectTags.js index d54df30cf4e7..709b5b891e8f 100644 --- a/packages/react-reconciler/src/ReactHookEffectTags.js +++ b/packages/react-reconciler/src/ReactHookEffectTags.js @@ -9,11 +9,11 @@ export type HookEffectTag = number; -export const NoEffect = /* */ 0b00000000; -export const UnmountSnapshot = /* */ 0b00000010; -export const UnmountMutation = /* */ 0b00000100; -export const MountMutation = /* */ 0b00001000; -export const UnmountLayout = /* */ 0b00010000; -export const MountLayout = /* */ 0b00100000; -export const MountPassive = /* */ 0b01000000; -export const UnmountPassive = /* */ 0b10000000; +export const NoEffect = /* */ 0b000; + +// Represents whether effect should fire. +export const HasEffect = /* */ 0b001; + +// Represents the phase in which the effect (not the clean-up) fires. +export const Layout = /* */ 0b010; +export const Passive = /* */ 0b100; diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index ca4d3090215e..c98dbb117127 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -43,6 +43,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.enableSchedulerTracing = true; ReactFeatureFlags.flushSuspenseFallbacksInTests = false; + ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true; React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); @@ -972,6 +973,90 @@ describe('ReactHooksWithNoopRenderer', () => { }, ); + it('defers passive effect destroy functions during unmount', () => { + function Child({bar, foo}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive bar create'); + return () => { + Scheduler.unstable_yieldValue('passive bar destroy'); + }; + }, [bar]); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout bar create'); + return () => { + Scheduler.unstable_yieldValue('layout bar destroy'); + }; + }, [bar]); + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive foo create'); + return () => { + Scheduler.unstable_yieldValue('passive foo destroy'); + }; + }, [foo]); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout foo create'); + return () => { + Scheduler.unstable_yieldValue('layout foo destroy'); + }; + }, [foo]); + Scheduler.unstable_yieldValue('render'); + return null; + } + + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'render', + 'layout bar create', + 'layout foo create', + 'Sync effect', + ]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield([ + 'passive bar create', + 'passive foo create', + ]); + }); + + // This update is exists to test an internal implementation detail: + // Effects without updating dependencies lose their layout/passive tag during an update. + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'render', + 'layout foo destroy', + 'layout foo create', + 'Sync effect', + ]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield([ + 'passive foo destroy', + 'passive foo create', + ]); + }); + + // Unmount the component and verify that passive destroy functions are deferred until post-commit. + act(() => { + ReactNoop.render(null, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'layout bar destroy', + 'layout foo destroy', + 'Sync effect', + ]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield([ + 'passive bar destroy', + 'passive foo destroy', + ]); + }); + }); + it('updates have async priority', () => { function Counter(props) { const [count, updateCount] = useState('(empty)'); @@ -1554,12 +1639,14 @@ describe('ReactHooksWithNoopRenderer', () => { 'Unmount B [0]', 'Mount A [1]', 'Oops!', - // Clean up effect A. There's no effect B to clean-up, because it - // never mounted. - 'Unmount A [1]', ]); expect(ReactNoop.getChildren()).toEqual([]); }); + expect(Scheduler).toHaveYielded([ + // Clean up effect A runs passively on unmount. + // There's no effect B to clean-up, because it never mounted. + 'Unmount A [1]', + ]); }); it('handles errors on unmount', () => { @@ -1599,13 +1686,12 @@ describe('ReactHooksWithNoopRenderer', () => { expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); - expect(Scheduler).toHaveYielded([ - 'Oops!', - // B unmounts even though an error was thrown in the previous effect - 'Unmount B [0]', - ]); - expect(ReactNoop.getChildren()).toEqual([]); + expect(Scheduler).toHaveYielded(['Oops!']); }); + // B unmounts even though an error was thrown in the previous effect + // B's destroy function runs later on unmount though, since it's passive + expect(Scheduler).toHaveYielded(['Unmount B [0]']); + expect(ReactNoop.getChildren()).toEqual([]); }); it('works with memo', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index d4997de38f70..361d8c146709 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -21,6 +21,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; ReactFeatureFlags.flushSuspenseFallbacksInTests = false; + ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true; React = require('react'); Fragment = React.Fragment; ReactNoop = require('react-noop-renderer'); @@ -1617,8 +1618,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(Scheduler).toFlushAndYield([ 'B', 'Destroy Layout Effect [Loading...]', - 'Destroy Effect [Loading...]', 'Layout Effect [B]', + 'Destroy Effect [Loading...]', 'Effect [B]', ]); @@ -1654,9 +1655,9 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(Scheduler).toFlushAndYield([ 'B2', 'Destroy Layout Effect [Loading...]', - 'Destroy Effect [Loading...]', 'Destroy Layout Effect [B]', 'Layout Effect [B2]', + 'Destroy Effect [Loading...]', 'Destroy Effect [B]', 'Effect [B2]', ]); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ead32bb435b4..4610a042b8ba 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -91,6 +91,12 @@ export const enableTrustedTypesIntegration = false; // Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance export const enableNativeTargetAsInstance = false; +// Controls behavior of deferred effect destroy functions during unmount. +// Previously these functions were run during commit (along with layout effects). +// Ideally we should delay these until after commit for performance reasons. +// This flag provides a killswitch if that proves to break existing code somehow. +export const deferPassiveEffectCleanupDuringUnmount = false; + // -------------------------- // Future APIs to be deprecated // -------------------------- diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 7ae4ab0e4113..324c229ff993 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -50,6 +50,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const deferPassiveEffectCleanupDuringUnmount = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 241dad2cf7e3..8359c153f984 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -45,6 +45,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const deferPassiveEffectCleanupDuringUnmount = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index a8bb6bf0aff5..c20f6e842a5e 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -45,6 +45,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const deferPassiveEffectCleanupDuringUnmount = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index ae94c8f7f5e7..65ca4e3e78da 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -45,6 +45,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const deferPassiveEffectCleanupDuringUnmount = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 60448b0097b0..cf4fbda7c7c2 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -43,6 +43,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const deferPassiveEffectCleanupDuringUnmount = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index cd3e59e5fe58..3b59401fcdc2 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -15,6 +15,7 @@ export const { debugRenderPhaseSideEffectsForStrictMode, disableInputAttributeSyncing, enableTrustedTypesIntegration, + deferPassiveEffectCleanupDuringUnmount, } = require('ReactFeatureFlags'); // In www, we have experimental support for gathering data