@samitouri / QOS-React-2 / commits / 7df32c4c8c

Flush `useEffect` clean up functions in the passive effects phase (#17925)

* Flush useEffect clean up functions in the passive effects phase This is a change in behavior that may cause broken product code, so it has been added behind a killswitch (deferPassiveEffectCleanupDuringUnmount) * Avoid scheduling unnecessary callbacks for cleanup effects Updated enqueuePendingPassiveEffectDestroyFn() to check rootDoesHavePassiveEffects before scheduling a new callback. This way we'll only schedule (at most) one. * Updated newly added test for added clarity. * Cleaned up hooks effect tags We previously used separate Mount* and Unmount* tags to track hooks work for each phase (snapshot, mutation, layout, and passive). This was somewhat complicated to trace through and there were man tag types we never even used (e.g. UnmountLayout, MountMutation, UnmountSnapshot). In addition to this, it left passive and layout hooks looking the same after renders without changed dependencies, which meant we were unable to reliably defer passive effect destroy functions until after the commit phase. This commit reduces the effect tag types to only include Layout and Passive and differentiates between work and no-work with an HasEffect flag. * Disabled deferred passive effects flushing in OSS builds for now * Split up unmount and mount effects list traversal

Brian Vaughn committed Feb 3, 2020 at 12:30 UTC 7df32c4c8c53d971e894c5e8d62a3cc908489b05
13 files changed +248 -87
packages/react-reconciler/src/ReactFiberCommitWork.js
+77 -44
@@ -26,6 +26,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
26
27 import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
28 import {
29 + deferPassiveEffectCleanupDuringUnmount,
30 enableSchedulerTracing,
31 enableProfilerTimer,
32 enableSuspenseServerRenderer,
@@ -109,16 +110,13 @@ import {
110 captureCommitPhaseError,
111 resolveRetryThenable,
112 markCommitTimeOfFallback,
113 + enqueuePendingPassiveEffectDestroyFn,
114 } from './ReactFiberWorkLoop';
115 import {
116 NoEffect as NoHookEffect,
115 - UnmountSnapshot,
116 - UnmountMutation,
117 - MountMutation,
118 - UnmountLayout,
119 - MountLayout,
120 - UnmountPassive,
121 - MountPassive,
117 + HasEffect as HookHasEffect,
118 + Layout as HookLayout,
119 + Passive as HookPassive,
120 } from './ReactHookEffectTags';
121 import {didWarnAboutReassigningProps} from './ReactFiberBeginWork';
122 import {runWithPriority, NormalPriority} from './SchedulerWithReactIntegration';
@@ -250,7 +248,6 @@ function commitBeforeMutationLifeCycles(
248 case ForwardRef:
249 case SimpleMemoComponent:
250 case Chunk: {
253 - commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
251 return;
252 }
253 case ClassComponent: {
@@ -328,18 +325,14 @@ function commitBeforeMutationLifeCycles(
325 );
326 }
327
331 -function commitHookEffectList(
332 - unmountTag: number,
333 - mountTag: number,
334 - finishedWork: Fiber,
335 -) {
328 +function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
329 const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
330 let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
331 if (lastEffect !== null) {
332 const firstEffect = lastEffect.next;
333 let effect = firstEffect;
334 do {
342 - if ((effect.tag & unmountTag) !== NoHookEffect) {
335 + if ((effect.tag & tag) === tag) {
336 // Unmount
337 const destroy = effect.destroy;
338 effect.destroy = undefined;
@@ -347,7 +340,19 @@ function commitHookEffectList(
340 destroy();
341 }
342 }
350 - if ((effect.tag & mountTag) !== NoHookEffect) {
343 + effect = effect.next;
344 + } while (effect !== firstEffect);
345 + }
346 +}
347 +
348 +function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
349 + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
350 + let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
351 + if (lastEffect !== null) {
352 + const firstEffect = lastEffect.next;
353 + let effect = firstEffect;
354 + do {
355 + if ((effect.tag & tag) === tag) {
356 // Mount
357 const create = effect.create;
358 effect.destroy = create();
@@ -398,8 +403,11 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void {
403 case ForwardRef:
404 case SimpleMemoComponent:
405 case Chunk: {
401 - commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
402 - commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
406 + // TODO (#17945) We should call all passive destroy functions (for all fibers)
407 + // before calling any create functions. The current approach only serializes
408 + // these for a single fiber.
409 + commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
410 + commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
411 break;
412 }
413 default:
@@ -419,7 +427,11 @@ function commitLifeCycles(
427 case ForwardRef:
428 case SimpleMemoComponent:
429 case Chunk: {
422 - commitHookEffectList(UnmountLayout, MountLayout, finishedWork);
430 + // At this point layout effects have already been destroyed (during mutation phase).
431 + // This is done to prevent sibling component effects from interfering with each other,
432 + // e.g. a destroy function in one component should never override a ref set
433 + // by a create function in another component during the same commit.
434 + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
435 return;
436 }
437 case ClassComponent: {
@@ -756,32 +768,47 @@ function commitUnmount(
768 if (lastEffect !== null) {
769 const firstEffect = lastEffect.next;
770
759 - // When the owner fiber is deleted, the destroy function of a passive
760 - // effect hook is called during the synchronous commit phase. This is
761 - // a concession to implementation complexity. Calling it in the
762 - // passive effect phase (like they usually are, when dependencies
763 - // change during an update) would require either traversing the
764 - // children of the deleted fiber again, or including unmount effects
765 - // as part of the fiber effect list.
766 - //
767 - // Because this is during the sync commit phase, we need to change
768 - // the priority.
769 - //
770 - // TODO: Reconsider this implementation trade off.
771 - const priorityLevel =
772 - renderPriorityLevel > NormalPriority
773 - ? NormalPriority
774 - : renderPriorityLevel;
775 - runWithPriority(priorityLevel, () => {
771 + if (deferPassiveEffectCleanupDuringUnmount) {
772 let effect = firstEffect;
773 do {
778 - const destroy = effect.destroy;
774 + const {destroy, tag} = effect;
775 if (destroy !== undefined) {
780 - safelyCallDestroy(current, destroy);
776 + if ((tag & HookPassive) !== NoHookEffect) {
777 + enqueuePendingPassiveEffectDestroyFn(destroy);
778 + } else {
779 + safelyCallDestroy(current, destroy);
780 + }
781 }
782 effect = effect.next;
783 } while (effect !== firstEffect);
784 - });
784 + } else {
785 + // When the owner fiber is deleted, the destroy function of a passive
786 + // effect hook is called during the synchronous commit phase. This is
787 + // a concession to implementation complexity. Calling it in the
788 + // passive effect phase (like they usually are, when dependencies
789 + // change during an update) would require either traversing the
790 + // children of the deleted fiber again, or including unmount effects
791 + // as part of the fiber effect list.
792 + //
793 + // Because this is during the sync commit phase, we need to change
794 + // the priority.
795 + //
796 + // TODO: Reconsider this implementation trade off.
797 + const priorityLevel =
798 + renderPriorityLevel > NormalPriority
799 + ? NormalPriority
800 + : renderPriorityLevel;
801 + runWithPriority(priorityLevel, () => {
802 + let effect = firstEffect;
803 + do {
804 + const destroy = effect.destroy;
805 + if (destroy !== undefined) {
806 + safelyCallDestroy(current, destroy);
807 + }
808 + effect = effect.next;
809 + } while (effect !== firstEffect);
810 + });
811 + }
812 }
813 }
814 return;
@@ -1285,9 +1312,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1312 case MemoComponent:
1313 case SimpleMemoComponent:
1314 case Chunk: {
1288 - // Note: We currently never use MountMutation, but useLayout uses
1289 - // UnmountMutation.
1290 - commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
1315 + // Layout effects are destroyed during the mutation phase so that all
1316 + // destroy functions for all fibers are called before any create functions.
1317 + // This prevents sibling component effects from interfering with each other,
1318 + // e.g. a destroy function in one component should never override a ref set
1319 + // by a create function in another component during the same commit.
1320 + commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1321 return;
1322 }
1323 case Profiler: {
@@ -1325,9 +1355,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1355 case MemoComponent:
1356 case SimpleMemoComponent:
1357 case Chunk: {
1328 - // Note: We currently never use MountMutation, but useLayout uses
1329 - // UnmountMutation.
1330 - commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
1358 + // Layout effects are destroyed during the mutation phase so that all
1359 + // destroy functions for all fibers are called before any create functions.
1360 + // This prevents sibling component effects from interfering with each other,
1361 + // e.g. a destroy function in one component should never override a ref set
1362 + // by a create function in another component during the same commit.
1363 + commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1364 return;
1365 }
1366 case ClassComponent: {
packages/react-reconciler/src/ReactFiberHooks.js
+22 -24
@@ -28,11 +28,9 @@ import {
28 Passive as PassiveEffect,
29 } from 'shared/ReactSideEffectTags';
30 import {
31 - NoEffect as NoHookEffect,
32 - UnmountMutation,
33 - MountLayout,
34 - UnmountPassive,
35 - MountPassive,
31 + HasEffect as HookHasEffect,
32 + Layout as HookLayout,
33 + Passive as HookPassive,
34 } from './ReactHookEffectTags';
35 import {
36 scheduleWork,
@@ -923,7 +921,12 @@ function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
921 const hook = mountWorkInProgressHook();
922 const nextDeps = deps === undefined ? null : deps;
923 currentlyRenderingFiber.effectTag |= fiberEffectTag;
926 - hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
924 + hook.memoizedState = pushEffect(
925 + HookHasEffect | hookEffectTag,
926 + create,
927 + undefined,
928 + nextDeps,
929 + );
930 }
931
932 function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
@@ -937,7 +940,7 @@ function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
940 if (nextDeps !== null) {
941 const prevDeps = prevEffect.deps;
942 if (areHookInputsEqual(nextDeps, prevDeps)) {
940 - pushEffect(NoHookEffect, create, destroy, nextDeps);
943 + pushEffect(hookEffectTag, create, destroy, nextDeps);
944 return;
945 }
946 }
@@ -945,7 +948,12 @@ function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
948
949 currentlyRenderingFiber.effectTag |= fiberEffectTag;
950
948 - hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
951 + hook.memoizedState = pushEffect(
952 + HookHasEffect | hookEffectTag,
953 + create,
954 + destroy,
955 + nextDeps,
956 + );
957 }
958
959 function mountEffect(
@@ -960,7 +968,7 @@ function mountEffect(
968 }
969 return mountEffectImpl(
970 UpdateEffect | PassiveEffect,
963 - UnmountPassive | MountPassive,
971 + HookPassive,
972 create,
973 deps,
974 );
@@ -978,7 +986,7 @@ function updateEffect(
986 }
987 return updateEffectImpl(
988 UpdateEffect | PassiveEffect,
981 - UnmountPassive | MountPassive,
989 + HookPassive,
990 create,
991 deps,
992 );
@@ -988,24 +996,14 @@ function mountLayoutEffect(
996 create: () => (() => void) | void,
997 deps: Array<mixed> | void | null,
998 ): void {
991 - return mountEffectImpl(
992 - UpdateEffect,
993 - UnmountMutation | MountLayout,
994 - create,
995 - deps,
996 - );
999 + return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1000 }
1001
1002 function updateLayoutEffect(
1003 create: () => (() => void) | void,
1004 deps: Array<mixed> | void | null,
1005 ): void {
1003 - return updateEffectImpl(
1004 - UpdateEffect,
1005 - UnmountMutation | MountLayout,
1006 - create,
1007 - deps,
1008 - );
1006 + return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
1007 }
1008
1009 function imperativeHandleEffect<T>(
@@ -1059,7 +1057,7 @@ function mountImperativeHandle<T>(
1057
1058 return mountEffectImpl(
1059 UpdateEffect,
1062 - UnmountMutation | MountLayout,
1060 + HookLayout,
1061 imperativeHandleEffect.bind(null, create, ref),
1062 effectDeps,
1063 );
@@ -1086,7 +1084,7 @@ function updateImperativeHandle<T>(
1084
1085 return updateEffectImpl(
1086 UpdateEffect,
1089 - UnmountMutation | MountLayout,
1087 + HookLayout,
1088 imperativeHandleEffect.bind(null, create, ref),
1089 effectDeps,
1090 );
packages/react-reconciler/src/ReactFiberWorkLoop.js
+31
@@ -18,6 +18,7 @@ import type {Hook} from './ReactFiberHooks';
18
19 import {
20 warnAboutDeprecatedLifecycles,
21 + deferPassiveEffectCleanupDuringUnmount,
22 enableUserTimingAPI,
23 enableSuspenseServerRenderer,
24 replayFailedUnitOfWorkWithInvokeGuardedCallback,
@@ -257,6 +258,7 @@ let rootDoesHavePassiveEffects: boolean = false;
258 let rootWithPendingPassiveEffects: FiberRoot | null = null;
259 let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority;
260 let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
261 +let pendingUnmountedPassiveEffectDestroyFunctions: Array<() => void> = [];
262
263 let rootsWithPendingDiscreteUpdates: Map<
264 FiberRoot,
@@ -2176,6 +2178,21 @@ export function flushPassiveEffects() {
2178 }
2179 }
2180
2181 +export function enqueuePendingPassiveEffectDestroyFn(
2182 + destroy: () => void,
2183 +): void {
2184 + if (deferPassiveEffectCleanupDuringUnmount) {
2185 + pendingUnmountedPassiveEffectDestroyFunctions.push(destroy);
2186 + if (!rootDoesHavePassiveEffects) {
2187 + rootDoesHavePassiveEffects = true;
2188 + scheduleCallback(NormalPriority, () => {
2189 + flushPassiveEffects();
2190 + return null;
2191 + });
2192 + }
2193 + }
2194 +}
2195 +
2196 function flushPassiveEffectsImpl() {
2197 if (rootWithPendingPassiveEffects === null) {
2198 return false;
@@ -2193,6 +2210,20 @@ function flushPassiveEffectsImpl() {
2210 executionContext |= CommitContext;
2211 const prevInteractions = pushInteractions(root);
2212
2213 + if (deferPassiveEffectCleanupDuringUnmount) {
2214 + // Flush any pending passive effect destroy functions that belong to
2215 + // components that were unmounted during the most recent commit.
2216 + for (
2217 + let i = 0;
2218 + i < pendingUnmountedPassiveEffectDestroyFunctions.length;
2219 + i++
2220 + ) {
2221 + const destroy = pendingUnmountedPassiveEffectDestroyFunctions[i];
2222 + invokeGuardedCallback(null, destroy, null);
2223 + }
2224 + pendingUnmountedPassiveEffectDestroyFunctions.length = 0;
2225 + }
2226 +
2227 // Note: This currently assumes there are no passive effects on the root
2228 // fiber, because the root is not part of its own effect list. This could
2229 // change in the future.
packages/react-reconciler/src/ReactHookEffectTags.js
+8 -8
@@ -9,11 +9,11 @@
9
10 export type HookEffectTag = number;
11
12 -export const NoEffect = /* */ 0b00000000;
13 -export const UnmountSnapshot = /* */ 0b00000010;
14 -export const UnmountMutation = /* */ 0b00000100;
15 -export const MountMutation = /* */ 0b00001000;
16 -export const UnmountLayout = /* */ 0b00010000;
17 -export const MountLayout = /* */ 0b00100000;
18 -export const MountPassive = /* */ 0b01000000;
19 -export const UnmountPassive = /* */ 0b10000000;
12 +export const NoEffect = /* */ 0b000;
13 +
14 +// Represents whether effect should fire.
15 +export const HasEffect = /* */ 0b001;
16 +
17 +// Represents the phase in which the effect (not the clean-up) fires.
18 +export const Layout = /* */ 0b010;
19 +export const Passive = /* */ 0b100;
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+95 -9
@@ -43,6 +43,7 @@ describe('ReactHooksWithNoopRenderer', () => {
43 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
44 ReactFeatureFlags.enableSchedulerTracing = true;
45 ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
46 + ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true;
47 React = require('react');
48 ReactNoop = require('react-noop-renderer');
49 Scheduler = require('scheduler');
@@ -972,6 +973,90 @@ describe('ReactHooksWithNoopRenderer', () => {
973 },
974 );
975
976 + it('defers passive effect destroy functions during unmount', () => {
977 + function Child({bar, foo}) {
978 + React.useEffect(() => {
979 + Scheduler.unstable_yieldValue('passive bar create');
980 + return () => {
981 + Scheduler.unstable_yieldValue('passive bar destroy');
982 + };
983 + }, [bar]);
984 + React.useLayoutEffect(() => {
985 + Scheduler.unstable_yieldValue('layout bar create');
986 + return () => {
987 + Scheduler.unstable_yieldValue('layout bar destroy');
988 + };
989 + }, [bar]);
990 + React.useEffect(() => {
991 + Scheduler.unstable_yieldValue('passive foo create');
992 + return () => {
993 + Scheduler.unstable_yieldValue('passive foo destroy');
994 + };
995 + }, [foo]);
996 + React.useLayoutEffect(() => {
997 + Scheduler.unstable_yieldValue('layout foo create');
998 + return () => {
999 + Scheduler.unstable_yieldValue('layout foo destroy');
1000 + };
1001 + }, [foo]);
1002 + Scheduler.unstable_yieldValue('render');
1003 + return null;
1004 + }
1005 +
1006 + act(() => {
1007 + ReactNoop.render(<Child bar={1} foo={1} />, () =>
1008 + Scheduler.unstable_yieldValue('Sync effect'),
1009 + );
1010 + expect(Scheduler).toFlushAndYieldThrough([
1011 + 'render',
1012 + 'layout bar create',
1013 + 'layout foo create',
1014 + 'Sync effect',
1015 + ]);
1016 + // Effects are deferred until after the commit
1017 + expect(Scheduler).toFlushAndYield([
1018 + 'passive bar create',
1019 + 'passive foo create',
1020 + ]);
1021 + });
1022 +
1023 + // This update is exists to test an internal implementation detail:
1024 + // Effects without updating dependencies lose their layout/passive tag during an update.
1025 + act(() => {
1026 + ReactNoop.render(<Child bar={1} foo={2} />, () =>
1027 + Scheduler.unstable_yieldValue('Sync effect'),
1028 + );
1029 + expect(Scheduler).toFlushAndYieldThrough([
1030 + 'render',
1031 + 'layout foo destroy',
1032 + 'layout foo create',
1033 + 'Sync effect',
1034 + ]);
1035 + // Effects are deferred until after the commit
1036 + expect(Scheduler).toFlushAndYield([
1037 + 'passive foo destroy',
1038 + 'passive foo create',
1039 + ]);
1040 + });
1041 +
1042 + // Unmount the component and verify that passive destroy functions are deferred until post-commit.
1043 + act(() => {
1044 + ReactNoop.render(null, () =>
1045 + Scheduler.unstable_yieldValue('Sync effect'),
1046 + );
1047 + expect(Scheduler).toFlushAndYieldThrough([
1048 + 'layout bar destroy',
1049 + 'layout foo destroy',
1050 + 'Sync effect',
1051 + ]);
1052 + // Effects are deferred until after the commit
1053 + expect(Scheduler).toFlushAndYield([
1054 + 'passive bar destroy',
1055 + 'passive foo destroy',
1056 + ]);
1057 + });
1058 + });
1059 +
1060 it('updates have async priority', () => {
1061 function Counter(props) {
1062 const [count, updateCount] = useState('(empty)');
@@ -1554,12 +1639,14 @@ describe('ReactHooksWithNoopRenderer', () => {
1639 'Unmount B [0]',
1640 'Mount A [1]',
1641 'Oops!',
1557 - // Clean up effect A. There's no effect B to clean-up, because it
1558 - // never mounted.
1559 - 'Unmount A [1]',
1642 ]);
1643 expect(ReactNoop.getChildren()).toEqual([]);
1644 });
1645 + expect(Scheduler).toHaveYielded([
1646 + // Clean up effect A runs passively on unmount.
1647 + // There's no effect B to clean-up, because it never mounted.
1648 + 'Unmount A [1]',
1649 + ]);
1650 });
1651
1652 it('handles errors on unmount', () => {
@@ -1599,13 +1686,12 @@ describe('ReactHooksWithNoopRenderer', () => {
1686 expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1687 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1688 expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1602 - expect(Scheduler).toHaveYielded([
1603 - 'Oops!',
1604 - // B unmounts even though an error was thrown in the previous effect
1605 - 'Unmount B [0]',
1606 - ]);
1607 - expect(ReactNoop.getChildren()).toEqual([]);
1689 + expect(Scheduler).toHaveYielded(['Oops!']);
1690 });
1691 + // B unmounts even though an error was thrown in the previous effect
1692 + // B's destroy function runs later on unmount though, since it's passive
1693 + expect(Scheduler).toHaveYielded(['Unmount B [0]']);
1694 + expect(ReactNoop.getChildren()).toEqual([]);
1695 });
1696
1697 it('works with memo', () => {
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+3 -2
@@ -21,6 +21,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
21 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23 ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
24 + ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true;
25 React = require('react');
26 Fragment = React.Fragment;
27 ReactNoop = require('react-noop-renderer');
@@ -1617,8 +1618,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1618 expect(Scheduler).toFlushAndYield([
1619 'B',
1620 'Destroy Layout Effect [Loading...]',
1620 - 'Destroy Effect [Loading...]',
1621 'Layout Effect [B]',
1622 + 'Destroy Effect [Loading...]',
1623 'Effect [B]',
1624 ]);
1625
@@ -1654,9 +1655,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1655 expect(Scheduler).toFlushAndYield([
1656 'B2',
1657 'Destroy Layout Effect [Loading...]',
1657 - 'Destroy Effect [Loading...]',
1658 'Destroy Layout Effect [B]',
1659 'Layout Effect [B2]',
1660 + 'Destroy Effect [Loading...]',
1661 'Destroy Effect [B]',
1662 'Effect [B2]',
1663 ]);
packages/shared/ReactFeatureFlags.js
+6
@@ -91,6 +91,12 @@ export const enableTrustedTypesIntegration = false;
91 // Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance
92 export const enableNativeTargetAsInstance = false;
93
94 +// Controls behavior of deferred effect destroy functions during unmount.
95 +// Previously these functions were run during commit (along with layout effects).
96 +// Ideally we should delay these until after commit for performance reasons.
97 +// This flag provides a killswitch if that proves to break existing code somehow.
98 +export const deferPassiveEffectCleanupDuringUnmount = false;
99 +
100 // --------------------------
101 // Future APIs to be deprecated
102 // --------------------------
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -50,6 +50,7 @@ export const disableTextareaChildren = false;
50 export const disableUnstableRenderSubtreeIntoContainer = false;
51 export const warnUnstableRenderSubtreeIntoContainer = false;
52 export const disableUnstableCreatePortal = false;
53 +export const deferPassiveEffectCleanupDuringUnmount = false;
54
55 // Only used in www builds.
56 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -45,6 +45,7 @@ export const disableTextareaChildren = false;
45 export const disableUnstableRenderSubtreeIntoContainer = false;
46 export const warnUnstableRenderSubtreeIntoContainer = false;
47 export const disableUnstableCreatePortal = false;
48 +export const deferPassiveEffectCleanupDuringUnmount = false;
49
50 // Only used in www builds.
51 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -45,6 +45,7 @@ export const disableTextareaChildren = false;
45 export const disableUnstableRenderSubtreeIntoContainer = false;
46 export const warnUnstableRenderSubtreeIntoContainer = false;
47 export const disableUnstableCreatePortal = false;
48 +export const deferPassiveEffectCleanupDuringUnmount = false;
49
50 // Only used in www builds.
51 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -45,6 +45,7 @@ export const disableTextareaChildren = false;
45 export const disableUnstableRenderSubtreeIntoContainer = false;
46 export const warnUnstableRenderSubtreeIntoContainer = false;
47 export const disableUnstableCreatePortal = false;
48 +export const deferPassiveEffectCleanupDuringUnmount = false;
49
50 // Only used in www builds.
51 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -43,6 +43,7 @@ export const disableTextareaChildren = false;
43 export const disableUnstableRenderSubtreeIntoContainer = false;
44 export const warnUnstableRenderSubtreeIntoContainer = false;
45 export const disableUnstableCreatePortal = false;
46 +export const deferPassiveEffectCleanupDuringUnmount = false;
47
48 // Only used in www builds.
49 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -15,6 +15,7 @@ export const {
15 debugRenderPhaseSideEffectsForStrictMode,
16 disableInputAttributeSyncing,
17 enableTrustedTypesIntegration,
18 + deferPassiveEffectCleanupDuringUnmount,
19 } = require('ReactFeatureFlags');
20
21 // In www, we have experimental support for gathering data