@samitouri / QOS-React-2 / commits / 23e5edd05c

[flags] clean up enableUseEffectEventHook (#35541)

This is landed everywhere

Ricky committed Jan 17, 2026 at 12:46 UTC 23e5edd05c1f89dc7a23452afabd02003c225858
17 files changed +309 -139
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
-3
@@ -6509,7 +6509,6 @@ describe('ReactDOMFizzServer', () => {
6509 });
6510
6511 describe('useEffectEvent', () => {
6512 - // @gate enableUseEffectEventHook
6512 it('can server render a component with useEffectEvent', async () => {
6513 const ref = React.createRef();
6514 function App() {
@@ -6540,7 +6539,6 @@ describe('ReactDOMFizzServer', () => {
6539 expect(getVisibleChildren(container)).toEqual(<button>1</button>);
6540 });
6541
6543 - // @gate enableUseEffectEventHook
6542 it('throws if useEffectEvent is called during a server render', async () => {
6543 const logs = [];
6544 function App() {
@@ -6572,7 +6570,6 @@ describe('ReactDOMFizzServer', () => {
6570 expect(reportedServerErrors).toEqual([caughtError]);
6571 });
6572
6575 - // @gate enableUseEffectEventHook
6573 it('does not guarantee useEffectEvent return values during server rendering are distinct', async () => {
6574 function App() {
6575 const onClick1 = React.useEffectEvent(() => {});
packages/react-reconciler/src/ReactFiberCommitWork.js
+22 -12
@@ -47,6 +47,7 @@ import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
47 import {
48 alwaysThrottleRetries,
49 enableCreateEventHandleAPI,
50 + enableEffectEventMutationPhase,
51 enableHiddenSubtreeInsertionEffectCleanup,
52 enableProfilerTimer,
53 enableProfilerCommitHooks,
@@ -54,7 +55,6 @@ import {
55 enableScopeAPI,
56 enableUpdaterTracking,
57 enableTransitionTracing,
57 - enableUseEffectEventHook,
58 enableLegacyHidden,
59 disableLegacyMode,
60 enableComponentPerformanceTrack,
@@ -500,17 +500,14 @@ function commitBeforeMutationEffectsOnFiber(
500 case FunctionComponent:
501 case ForwardRef:
502 case SimpleMemoComponent: {
503 - if (enableUseEffectEventHook) {
504 - if ((flags & Update) !== NoFlags) {
505 - const updateQueue: FunctionComponentUpdateQueue | null =
506 - (finishedWork.updateQueue: any);
507 - const eventPayloads =
508 - updateQueue !== null ? updateQueue.events : null;
509 - if (eventPayloads !== null) {
510 - for (let ii = 0; ii < eventPayloads.length; ii++) {
511 - const {ref, nextImpl} = eventPayloads[ii];
512 - ref.impl = nextImpl;
513 - }
503 + if (!enableEffectEventMutationPhase && (flags & Update) !== NoFlags) {
504 + const updateQueue: FunctionComponentUpdateQueue | null =
505 + (finishedWork.updateQueue: any);
506 + const eventPayloads = updateQueue !== null ? updateQueue.events : null;
507 + if (eventPayloads !== null) {
508 + for (let ii = 0; ii < eventPayloads.length; ii++) {
509 + const {ref, nextImpl} = eventPayloads[ii];
510 + ref.impl = nextImpl;
511 }
512 }
513 }
@@ -2050,6 +2047,19 @@ function commitMutationEffectsOnFiber(
2047 commitReconciliationEffects(finishedWork, lanes);
2048
2049 if (flags & Update) {
2050 + // Mutate event effect callbacks before insertion effects.
2051 + if (enableEffectEventMutationPhase) {
2052 + const updateQueue: FunctionComponentUpdateQueue | null =
2053 + (finishedWork.updateQueue: any);
2054 + const eventPayloads =
2055 + updateQueue !== null ? updateQueue.events : null;
2056 + if (eventPayloads !== null) {
2057 + for (let ii = 0; ii < eventPayloads.length; ii++) {
2058 + const {ref, nextImpl} = eventPayloads[ii];
2059 + ref.impl = nextImpl;
2060 + }
2061 + }
2062 + }
2063 commitHookEffectListUnmount(
2064 HookInsertion | HookHasEffect,
2065 finishedWork,
packages/react-reconciler/src/ReactFiberFlags.js
+6 -7
@@ -9,7 +9,7 @@
9
10 import {
11 enableCreateEventHandleAPI,
12 - enableUseEffectEventHook,
12 + enableEffectEventMutationPhase,
13 } from 'shared/ReactFeatureFlags';
14
15 export type Flags = number;
@@ -102,12 +102,11 @@ export const BeforeMutationMask: number =
102 // TODO: Only need to visit Deletions during BeforeMutation phase if an
103 // element is focused.
104 Update | ChildDeletion | Visibility
105 - : enableUseEffectEventHook
106 - ? // TODO: The useEffectEvent hook uses the snapshot phase for clean up but it
107 - // really should use the mutation phase for this or at least schedule an
108 - // explicit Snapshot phase flag for this.
109 - Update
110 - : 0);
105 + : // useEffectEvent uses the snapshot phase,
106 + // but we're moving it to the mutation phase.
107 + enableEffectEventMutationPhase
108 + ? 0
109 + : Update);
110
111 // For View Transition support we use the snapshot phase to scan the tree for potentially
112 // affected ViewTransition components.
packages/react-reconciler/src/ReactFiberHooks.js
+56 -86
@@ -38,7 +38,6 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
38 import {
39 enableSchedulingProfiler,
40 enableTransitionTracing,
41 - enableUseEffectEventHook,
41 enableLegacyCache,
42 disableLegacyMode,
43 enableNoCloningMemoCache,
@@ -3893,10 +3892,8 @@ export const ContextOnlyDispatcher: Dispatcher = {
3892 useOptimistic: throwInvalidHookError,
3893 useMemoCache: throwInvalidHookError,
3894 useCacheRefresh: throwInvalidHookError,
3895 + useEffectEvent: throwInvalidHookError,
3896 };
3897 -if (enableUseEffectEventHook) {
3898 - (ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
3899 -}
3897
3898 const HooksDispatcherOnMount: Dispatcher = {
3899 readContext,
@@ -3923,10 +3920,8 @@ const HooksDispatcherOnMount: Dispatcher = {
3920 useOptimistic: mountOptimistic,
3921 useMemoCache,
3922 useCacheRefresh: mountRefresh,
3923 + useEffectEvent: mountEvent,
3924 };
3927 -if (enableUseEffectEventHook) {
3928 - (HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
3929 -}
3925
3926 const HooksDispatcherOnUpdate: Dispatcher = {
3927 readContext,
@@ -3953,10 +3948,8 @@ const HooksDispatcherOnUpdate: Dispatcher = {
3948 useOptimistic: updateOptimistic,
3949 useMemoCache,
3950 useCacheRefresh: updateRefresh,
3951 + useEffectEvent: updateEvent,
3952 };
3957 -if (enableUseEffectEventHook) {
3958 - (HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
3959 -}
3953
3954 const HooksDispatcherOnRerender: Dispatcher = {
3955 readContext,
@@ -3983,10 +3976,8 @@ const HooksDispatcherOnRerender: Dispatcher = {
3976 useOptimistic: rerenderOptimistic,
3977 useMemoCache,
3978 useCacheRefresh: updateRefresh,
3979 + useEffectEvent: updateEvent,
3980 };
3987 -if (enableUseEffectEventHook) {
3988 - (HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
3989 -}
3981
3982 let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
3983 let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -4176,17 +4167,14 @@ if (__DEV__) {
4167 mountHookTypesDev();
4168 return mountRefresh();
4169 },
4170 + useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4171 + callback: F,
4172 + ): F {
4173 + currentHookNameInDev = 'useEffectEvent';
4174 + mountHookTypesDev();
4175 + return mountEvent(callback);
4176 + },
4177 };
4180 - if (enableUseEffectEventHook) {
4181 - (HooksDispatcherOnMountInDEV: Dispatcher).useEffectEvent =
4182 - function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4183 - callback: F,
4184 - ): F {
4185 - currentHookNameInDev = 'useEffectEvent';
4186 - mountHookTypesDev();
4187 - return mountEvent(callback);
4188 - };
4189 - }
4178
4179 HooksDispatcherOnMountWithHookTypesInDEV = {
4180 readContext<T>(context: ReactContext<T>): T {
@@ -4343,17 +4331,14 @@ if (__DEV__) {
4331 updateHookTypesDev();
4332 return mountRefresh();
4333 },
4334 + useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4335 + callback: F,
4336 + ): F {
4337 + currentHookNameInDev = 'useEffectEvent';
4338 + updateHookTypesDev();
4339 + return mountEvent(callback);
4340 + },
4341 };
4347 - if (enableUseEffectEventHook) {
4348 - (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEffectEvent =
4349 - function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4350 - callback: F,
4351 - ): F {
4352 - currentHookNameInDev = 'useEffectEvent';
4353 - updateHookTypesDev();
4354 - return mountEvent(callback);
4355 - };
4356 - }
4342
4343 HooksDispatcherOnUpdateInDEV = {
4344 readContext<T>(context: ReactContext<T>): T {
@@ -4510,17 +4495,14 @@ if (__DEV__) {
4495 updateHookTypesDev();
4496 return updateRefresh();
4497 },
4498 + useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4499 + callback: F,
4500 + ): F {
4501 + currentHookNameInDev = 'useEffectEvent';
4502 + updateHookTypesDev();
4503 + return updateEvent(callback);
4504 + },
4505 };
4514 - if (enableUseEffectEventHook) {
4515 - (HooksDispatcherOnUpdateInDEV: Dispatcher).useEffectEvent =
4516 - function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4517 - callback: F,
4518 - ): F {
4519 - currentHookNameInDev = 'useEffectEvent';
4520 - updateHookTypesDev();
4521 - return updateEvent(callback);
4522 - };
4523 - }
4506
4507 HooksDispatcherOnRerenderInDEV = {
4508 readContext<T>(context: ReactContext<T>): T {
@@ -4677,17 +4659,14 @@ if (__DEV__) {
4659 updateHookTypesDev();
4660 return updateRefresh();
4661 },
4662 + useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4663 + callback: F,
4664 + ): F {
4665 + currentHookNameInDev = 'useEffectEvent';
4666 + updateHookTypesDev();
4667 + return updateEvent(callback);
4668 + },
4669 };
4681 - if (enableUseEffectEventHook) {
4682 - (HooksDispatcherOnRerenderInDEV: Dispatcher).useEffectEvent =
4683 - function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4684 - callback: F,
4685 - ): F {
4686 - currentHookNameInDev = 'useEffectEvent';
4687 - updateHookTypesDev();
4688 - return updateEvent(callback);
4689 - };
4690 - }
4670
4671 InvalidNestedHooksDispatcherOnMountInDEV = {
4672 readContext<T>(context: ReactContext<T>): T {
@@ -4868,18 +4847,15 @@ if (__DEV__) {
4847 mountHookTypesDev();
4848 return mountRefresh();
4849 },
4850 + useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4851 + callback: F,
4852 + ): F {
4853 + currentHookNameInDev = 'useEffectEvent';
4854 + warnInvalidHookAccess();
4855 + mountHookTypesDev();
4856 + return mountEvent(callback);
4857 + },
4858 };
4872 - if (enableUseEffectEventHook) {
4873 - (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEffectEvent =
4874 - function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
4875 - callback: F,
4876 - ): F {
4877 - currentHookNameInDev = 'useEffectEvent';
4878 - warnInvalidHookAccess();
4879 - mountHookTypesDev();
4880 - return mountEvent(callback);
4881 - };
4882 - }
4859
4860 InvalidNestedHooksDispatcherOnUpdateInDEV = {
4861 readContext<T>(context: ReactContext<T>): T {
@@ -5060,18 +5036,15 @@ if (__DEV__) {
5036 updateHookTypesDev();
5037 return updateRefresh();
5038 },
5039 + useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
5040 + callback: F,
5041 + ): F {
5042 + currentHookNameInDev = 'useEffectEvent';
5043 + warnInvalidHookAccess();
5044 + updateHookTypesDev();
5045 + return updateEvent(callback);
5046 + },
5047 };
5064 - if (enableUseEffectEventHook) {
5065 - (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEffectEvent =
5066 - function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
5067 - callback: F,
5068 - ): F {
5069 - currentHookNameInDev = 'useEffectEvent';
5070 - warnInvalidHookAccess();
5071 - updateHookTypesDev();
5072 - return updateEvent(callback);
5073 - };
5074 - }
5048
5049 InvalidNestedHooksDispatcherOnRerenderInDEV = {
5050 readContext<T>(context: ReactContext<T>): T {
@@ -5252,16 +5225,13 @@ if (__DEV__) {
5225 updateHookTypesDev();
5226 return updateRefresh();
5227 },
5228 + useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
5229 + callback: F,
5230 + ): F {
5231 + currentHookNameInDev = 'useEffectEvent';
5232 + warnInvalidHookAccess();
5233 + updateHookTypesDev();
5234 + return updateEvent(callback);
5235 + },
5236 };
5256 - if (enableUseEffectEventHook) {
5257 - (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEffectEvent =
5258 - function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
5259 - callback: F,
5260 - ): F {
5261 - currentHookNameInDev = 'useEffectEvent';
5262 - warnInvalidHookAccess();
5263 - updateHookTypesDev();
5264 - return updateEvent(callback);
5265 - };
5266 - }
5237 }
packages/react-reconciler/src/ReactInternalTypes.js
+1 -2
@@ -409,8 +409,7 @@ export type Dispatcher = {
409 create: () => (() => void) | void,
410 deps: Array<mixed> | void | null,
411 ): void,
412 - // TODO: Non-nullable once `enableUseEffectEventHook` is on everywhere.
413 - useEffectEvent?: <Args, F: (...Array<Args>) => mixed>(callback: F) => F,
412 + useEffectEvent: <Args, F: (...Array<Args>) => mixed>(callback: F) => F,
413 useInsertionEffect(
414 create: () => (() => void) | void,
415 deps: Array<mixed> | void | null,
packages/react-reconciler/src/__tests__/useEffectEvent-test.js
+208 -12
@@ -53,7 +53,6 @@ describe('useEffectEvent', () => {
53 return <span prop={props.text} />;
54 }
55
56 - // @gate enableUseEffectEventHook
56 it('memoizes basic case correctly', async () => {
57 class IncrementButton extends React.PureComponent {
58 increment = () => {
@@ -129,7 +128,6 @@ describe('useEffectEvent', () => {
128 );
129 });
130
132 - // @gate enableUseEffectEventHook
131 it('can be defined more than once', async () => {
132 class IncrementButton extends React.PureComponent {
133 increment = () => {
@@ -191,7 +189,6 @@ describe('useEffectEvent', () => {
189 );
190 });
191
194 - // @gate enableUseEffectEventHook
192 it('does not preserve `this` in event functions', async () => {
193 class GreetButton extends React.PureComponent {
194 greet = () => {
@@ -241,7 +238,6 @@ describe('useEffectEvent', () => {
238 );
239 });
240
244 - // @gate enableUseEffectEventHook
241 it('throws when called in render', async () => {
242 class IncrementButton extends React.PureComponent {
243 increment = () => {
@@ -275,7 +271,6 @@ describe('useEffectEvent', () => {
271 assertLog([]);
272 });
273
278 - // @gate enableUseEffectEventHook
274 it("useLayoutEffect shouldn't re-fire when event handlers change", async () => {
275 class IncrementButton extends React.PureComponent {
276 increment = () => {
@@ -375,7 +370,6 @@ describe('useEffectEvent', () => {
370 );
371 });
372
378 - // @gate enableUseEffectEventHook
373 it("useEffect shouldn't re-fire when event handlers change", async () => {
374 class IncrementButton extends React.PureComponent {
375 increment = () => {
@@ -474,7 +468,6 @@ describe('useEffectEvent', () => {
468 );
469 });
470
477 - // @gate enableUseEffectEventHook
471 it('is stable in a custom hook', async () => {
472 class IncrementButton extends React.PureComponent {
473 increment = () => {
@@ -579,7 +572,6 @@ describe('useEffectEvent', () => {
572 );
573 });
574
582 - // @gate enableUseEffectEventHook
575 it('is mutated before all other effects', async () => {
576 function Counter({value}) {
577 useInsertionEffect(() => {
@@ -603,7 +595,214 @@ describe('useEffectEvent', () => {
595 assertLog(['Effect value: 2', 'Event value: 2']);
596 });
597
606 - // @gate enableUseEffectEventHook
598 + it('updates parent and child event effects before their respective effect lifecycles', async () => {
599 + function Parent({value}) {
600 + const parentEvent = useEffectEvent(() => {
601 + Scheduler.log('Parent event: ' + value);
602 + });
603 +
604 + useInsertionEffect(() => {
605 + Scheduler.log('Parent insertion');
606 + parentEvent();
607 + }, [value]);
608 +
609 + return <Child value={value} />;
610 + }
611 +
612 + function Child({value}) {
613 + const childEvent = useEffectEvent(() => {
614 + Scheduler.log('Child event: ' + value);
615 + });
616 +
617 + useInsertionEffect(() => {
618 + Scheduler.log('Child insertion');
619 + childEvent();
620 + }, [value]);
621 +
622 + return null;
623 + }
624 +
625 + ReactNoop.render(<Parent value={1} />);
626 + await waitForAll([
627 + 'Child insertion',
628 + 'Child event: 1',
629 + 'Parent insertion',
630 + 'Parent event: 1',
631 + ]);
632 +
633 + await act(() => ReactNoop.render(<Parent value={2} />));
634 + // Each component's event is updated before its own insertion effect runs
635 + assertLog([
636 + 'Child insertion',
637 + 'Child event: 2',
638 + 'Parent insertion',
639 + 'Parent event: 2',
640 + ]);
641 + });
642 +
643 + it('fires all insertion effects (interleaved) with useEffectEvent before firing any layout effects', async () => {
644 + // This test mirrors the 'fires all insertion effects (interleaved) before firing any layout effects'
645 + // test in ReactHooksWithNoopRenderer-test.js, but adds useEffectEvent to verify that
646 + // event payloads are updated before each component's insertion effects run.
647 + // It also includes passive effects to verify the full effect lifecycle.
648 + let committedA = '(empty)';
649 + let committedB = '(empty)';
650 +
651 + function CounterA(props) {
652 + const onEvent = useEffectEvent(() => {
653 + return `Event A [A: ${committedA}, B: ${committedB}]`;
654 + });
655 +
656 + useInsertionEffect(() => {
657 + // Call the event function to verify it sees the latest value
658 + Scheduler.log(
659 + `Create Insertion A [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
660 + );
661 + committedA = String(props.count);
662 + return () => {
663 + Scheduler.log(
664 + `Destroy Insertion A [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
665 + );
666 + };
667 + });
668 +
669 + useLayoutEffect(() => {
670 + Scheduler.log(
671 + `Create Layout A [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
672 + );
673 + return () => {
674 + Scheduler.log(
675 + `Destroy Layout A [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
676 + );
677 + };
678 + });
679 +
680 + useEffect(() => {
681 + Scheduler.log(
682 + `Create Passive A [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
683 + );
684 + return () => {
685 + Scheduler.log(
686 + `Destroy Passive A [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
687 + );
688 + };
689 + });
690 +
691 + return null;
692 + }
693 +
694 + function CounterB(props) {
695 + const onEvent = useEffectEvent(() => {
696 + return `Event B [A: ${committedA}, B: ${committedB}]`;
697 + });
698 +
699 + useInsertionEffect(() => {
700 + // Call the event function to verify it sees the latest value
701 + Scheduler.log(
702 + `Create Insertion B [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
703 + );
704 + committedB = String(props.count);
705 + return () => {
706 + Scheduler.log(
707 + `Destroy Insertion B [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
708 + );
709 + };
710 + });
711 +
712 + useLayoutEffect(() => {
713 + Scheduler.log(
714 + `Create Layout B [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
715 + );
716 + return () => {
717 + Scheduler.log(
718 + `Destroy Layout B [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
719 + );
720 + };
721 + });
722 +
723 + useEffect(() => {
724 + Scheduler.log(
725 + `Create Passive B [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
726 + );
727 + return () => {
728 + Scheduler.log(
729 + `Destroy Passive B [A: ${committedA}, B: ${committedB}], event: ${onEvent()}`,
730 + );
731 + };
732 + });
733 +
734 + return null;
735 + }
736 +
737 + await act(async () => {
738 + ReactNoop.render(
739 + <React.Fragment>
740 + <CounterA count={0} />
741 + <CounterB count={0} />
742 + </React.Fragment>,
743 + );
744 + // All insertion effects fire before all layout effects, then passive effects
745 + // Event functions should see the state AT THE TIME they're called
746 + await waitForAll([
747 + // Insertion effects (mutation phase)
748 + 'Create Insertion A [A: (empty), B: (empty)], event: Event A [A: (empty), B: (empty)]',
749 + 'Create Insertion B [A: 0, B: (empty)], event: Event B [A: 0, B: (empty)]',
750 + // Layout effects
751 + 'Create Layout A [A: 0, B: 0], event: Event A [A: 0, B: 0]',
752 + 'Create Layout B [A: 0, B: 0], event: Event B [A: 0, B: 0]',
753 + // Passive effects
754 + 'Create Passive A [A: 0, B: 0], event: Event A [A: 0, B: 0]',
755 + 'Create Passive B [A: 0, B: 0], event: Event B [A: 0, B: 0]',
756 + ]);
757 + expect([committedA, committedB]).toEqual(['0', '0']);
758 + });
759 +
760 + await act(async () => {
761 + ReactNoop.render(
762 + <React.Fragment>
763 + <CounterA count={1} />
764 + <CounterB count={1} />
765 + </React.Fragment>,
766 + );
767 + await waitForAll([
768 + // Component A: insertion destroy, then create
769 + 'Destroy Insertion A [A: 0, B: 0], event: Event A [A: 0, B: 0]',
770 + 'Create Insertion A [A: 0, B: 0], event: Event A [A: 0, B: 0]',
771 + // Component A: layout destroy (after insertion updated committedA)
772 + 'Destroy Layout A [A: 1, B: 0], event: Event A [A: 1, B: 0]',
773 + // Component B: insertion destroy, then create
774 + 'Destroy Insertion B [A: 1, B: 0], event: Event B [A: 1, B: 0]',
775 + 'Create Insertion B [A: 1, B: 0], event: Event B [A: 1, B: 0]',
776 + // Component B: layout destroy (after insertion updated committedB)
777 + 'Destroy Layout B [A: 1, B: 1], event: Event B [A: 1, B: 1]',
778 + // Layout creates
779 + 'Create Layout A [A: 1, B: 1], event: Event A [A: 1, B: 1]',
780 + 'Create Layout B [A: 1, B: 1], event: Event B [A: 1, B: 1]',
781 + // Passive destroys then creates
782 + 'Destroy Passive A [A: 1, B: 1], event: Event A [A: 1, B: 1]',
783 + 'Destroy Passive B [A: 1, B: 1], event: Event B [A: 1, B: 1]',
784 + 'Create Passive A [A: 1, B: 1], event: Event A [A: 1, B: 1]',
785 + 'Create Passive B [A: 1, B: 1], event: Event B [A: 1, B: 1]',
786 + ]);
787 + expect([committedA, committedB]).toEqual(['1', '1']);
788 + });
789 +
790 + // Unmount everything
791 + await act(async () => {
792 + ReactNoop.render(null);
793 + await waitForAll([
794 + // Insertion and layout destroys (mutation/layout phase)
795 + 'Destroy Insertion A [A: 1, B: 1], event: Event A [A: 1, B: 1]',
796 + 'Destroy Layout A [A: 1, B: 1], event: Event A [A: 1, B: 1]',
797 + 'Destroy Insertion B [A: 1, B: 1], event: Event B [A: 1, B: 1]',
798 + 'Destroy Layout B [A: 1, B: 1], event: Event B [A: 1, B: 1]',
799 + // Passive destroys
800 + 'Destroy Passive A [A: 1, B: 1], event: Event A [A: 1, B: 1]',
801 + 'Destroy Passive B [A: 1, B: 1], event: Event B [A: 1, B: 1]',
802 + ]);
803 + });
804 + });
805 +
806 it("doesn't provide a stable identity", async () => {
807 function Counter({shouldRender, value}) {
808 const onClick = useEffectEvent(() => {
@@ -642,7 +841,6 @@ describe('useEffectEvent', () => {
841 ]);
842 });
843
645 - // @gate enableUseEffectEventHook
844 it('event handlers always see the latest committed value', async () => {
845 let committedEventHandler = null;
846
@@ -692,7 +890,6 @@ describe('useEffectEvent', () => {
890 expect(committedEventHandler()).toBe('Value seen by useEffectEvent: 2');
891 });
892
695 - // @gate enableUseEffectEventHook
893 it('integration: implements docs chat room example', async () => {
894 function createConnection() {
895 let connectedCallback;
@@ -781,7 +978,6 @@ describe('useEffectEvent', () => {
978 );
979 });
980
784 - // @gate enableUseEffectEventHook
981 it('integration: implements the docs logVisit example', async () => {
982 class AddToCartButton extends React.PureComponent {
983 addToCart = () => {
packages/react-server/src/ReactFizzHooks.js
+2 -5
@@ -38,7 +38,6 @@ import {
38 } from './ReactFizzConfig';
39 import {createFastHash} from './ReactServerStreamConfig';
40
41 -import {enableUseEffectEventHook} from 'shared/ReactFeatureFlags';
41 import is from 'shared/objectIs';
42 import {
43 REACT_CONTEXT_TYPE,
@@ -833,6 +832,7 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
832 useHostTransitionStatus,
833 useMemoCache,
834 useCacheRefresh,
835 + useEffectEvent,
836 }
837 : {
838 readContext,
@@ -858,12 +858,9 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
858 useOptimistic,
859 useMemoCache,
860 useCacheRefresh,
861 + useEffectEvent,
862 };
863
863 -if (enableUseEffectEventHook) {
864 - HooksDispatcher.useEffectEvent = useEffectEvent;
865 -}
866 -
864 export let currentResumableState: null | ResumableState = (null: any);
865 export function setCurrentResumableState(
866 resumableState: null | ResumableState,
packages/react-server/src/ReactFlightHooks.js
+1 -4
@@ -17,7 +17,6 @@ import {
17 } from 'shared/ReactSymbols';
18 import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
19 import {isClientReference} from './ReactFlightServerConfig';
20 -import {enableUseEffectEventHook} from 'shared/ReactFeatureFlags';
20
21 let currentRequest = null;
22 let thenableIndexCounter = 0;
@@ -101,10 +100,8 @@ export const HooksDispatcher: Dispatcher = {
100 useCacheRefresh(): <T>(?() => T, ?T) => void {
101 return unsupportedRefresh;
102 },
103 + useEffectEvent: (unsupportedHook: any),
104 };
105 -if (enableUseEffectEventHook) {
106 - HooksDispatcher.useEffectEvent = (unsupportedHook: any);
107 -}
105
106 function unsupportedHook(): void {
107 throw new Error('This Hook is not supported in Server Components.');
packages/shared/ReactFeatureFlags.js
+4 -2
@@ -118,8 +118,6 @@ export const enableCPUSuspense = __EXPERIMENTAL__;
118 // Test this at Meta before enabling.
119 export const enableNoCloningMemoCache: boolean = false;
120
121 -export const enableUseEffectEventHook: boolean = true;
122 -
121 // Test in www before enabling in open source.
122 // Enables DOM-server to stream its instruction set as data-attributes
123 // (handled with an MutationObserver) instead of inline-scripts
@@ -127,6 +125,10 @@ export const enableFizzExternalRuntime = __EXPERIMENTAL__;
125
126 export const alwaysThrottleRetries: boolean = true;
127
128 +// Gate whether useEffectEvent uses the mutation phase (true) or before-mutation
129 +// phase (false) for updating event function references.
130 +export const enableEffectEventMutationPhase: boolean = false;
131 +
132 export const passChildrenWhenCloningPersistedNodes: boolean = false;
133
134 export const enableEagerAlternateStateNodeCleanup: boolean = true;
packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
+1
@@ -27,3 +27,4 @@ export const enableFragmentRefs = __VARIANT__;
27 export const enableFragmentRefsScrollIntoView = __VARIANT__;
28 export const enableFragmentRefsInstanceHandles = __VARIANT__;
29 export const enableComponentPerformanceTrack = __VARIANT__;
30 +export const enableEffectEventMutationPhase = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1 -1
@@ -20,6 +20,7 @@ const dynamicFlags: DynamicExportsType = (dynamicFlagsUntyped: any);
20 // the exports object every time a flag is read.
21 export const {
22 alwaysThrottleRetries,
23 + enableEffectEventMutationPhase,
24 enableHiddenSubtreeInsertionEffectCleanup,
25 enableObjectFiber,
26 enableEagerAlternateStateNodeCleanup,
@@ -64,7 +65,6 @@ export const enableTaint: boolean = true;
65 export const enableTransitionTracing: boolean = false;
66 export const enableTrustedTypesIntegration: boolean = false;
67 export const enableUpdaterTracking: boolean = __PROFILE__;
67 -export const enableUseEffectEventHook: boolean = true;
68 export const retryLaneExpirationMs = 5000;
69 export const syncLaneExpirationMs = 250;
70 export const transitionLaneExpirationMs = 5000;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1 -1
@@ -46,12 +46,12 @@ export const enableSchedulingProfiler: boolean =
46 !enableComponentPerformanceTrack && __PROFILE__;
47 export const enableScopeAPI: boolean = false;
48 export const enableEagerAlternateStateNodeCleanup: boolean = true;
49 +export const enableEffectEventMutationPhase: boolean = false;
50 export const enableSuspenseAvoidThisFallback: boolean = false;
51 export const enableSuspenseCallback: boolean = false;
52 export const enableTaint: boolean = true;
53 export const enableTransitionTracing: boolean = false;
54 export const enableTrustedTypesIntegration: boolean = false;
54 -export const enableUseEffectEventHook: boolean = true;
55 export const passChildrenWhenCloningPersistedNodes: boolean = false;
56 export const renameElementSymbol: boolean = true;
57 export const retryLaneExpirationMs = 5000;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1 -1
@@ -32,7 +32,6 @@ export const disableTextareaChildren: boolean = false;
32 export const enableSuspenseAvoidThisFallback: boolean = false;
33 export const enableCPUSuspense: boolean = false;
34 export const enableNoCloningMemoCache: boolean = false;
35 -export const enableUseEffectEventHook: boolean = true;
35 export const enableLegacyFBSupport: boolean = false;
36 export const enableMoveBefore: boolean = false;
37 export const enableHiddenSubtreeInsertionEffectCleanup: boolean = false;
@@ -59,6 +58,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
58
59 export const renameElementSymbol: boolean = true;
60 export const enableEagerAlternateStateNodeCleanup: boolean = true;
61 +export const enableEffectEventMutationPhase: boolean = false;
62
63 export const enableYieldingBeforePassive: boolean = true;
64
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1 -1
@@ -43,13 +43,13 @@ export const enableComponentPerformanceTrack = false;
43 export const enablePerformanceIssueReporting = false;
44 export const enableScopeAPI = false;
45 export const enableEagerAlternateStateNodeCleanup = true;
46 +export const enableEffectEventMutationPhase = false;
47 export const enableSuspenseAvoidThisFallback = false;
48 export const enableSuspenseCallback = false;
49 export const enableTaint = true;
50 export const enableTransitionTracing = false;
51 export const enableTrustedTypesIntegration = false;
52 export const enableUpdaterTracking = false;
52 -export const enableUseEffectEventHook = true;
53 export const passChildrenWhenCloningPersistedNodes = false;
54 export const renameElementSymbol = false;
55 export const retryLaneExpirationMs = 5000;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1 -1
@@ -34,7 +34,6 @@ export const disableTextareaChildren: boolean = false;
34 export const enableSuspenseAvoidThisFallback: boolean = true;
35 export const enableCPUSuspense: boolean = false;
36 export const enableNoCloningMemoCache: boolean = false;
37 -export const enableUseEffectEventHook: boolean = true;
37 export const enableLegacyFBSupport: boolean = false;
38 export const enableMoveBefore: boolean = false;
39 export const enableHiddenSubtreeInsertionEffectCleanup: boolean = true;
@@ -65,6 +64,7 @@ export const renameElementSymbol: boolean = false;
64
65 export const enableObjectFiber: boolean = false;
66 export const enableEagerAlternateStateNodeCleanup: boolean = true;
67 +export const enableEffectEventMutationPhase: boolean = false;
68
69 export const enableHydrationLaneScheduling: boolean = true;
70
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+2
@@ -40,6 +40,8 @@ export const enableAsyncDebugInfo: boolean = __VARIANT__;
40
41 export const enableInternalInstanceMap: boolean = __VARIANT__;
42
43 +export const enableEffectEventMutationPhase: boolean = __VARIANT__;
44 +
45 // TODO: These flags are hard-coded to the default values used in open source.
46 // Update the tests so that they pass in either mode, then set these
47 // to __VARIANT__.
packages/shared/forks/ReactFeatureFlags.www.js
+1 -1
@@ -18,6 +18,7 @@ export const {
18 alwaysThrottleRetries,
19 disableLegacyContextForFunctionComponents,
20 disableSchedulerTimeoutInWorkLoop,
21 + enableEffectEventMutationPhase,
22 enableHiddenSubtreeInsertionEffectCleanup,
23 enableInfiniteRenderLoopDetection,
24 enableNoCloningMemoCache,
@@ -49,7 +50,6 @@ export const enableUpdaterTracking = __PROFILE__;
50 export const enableSuspenseAvoidThisFallback: boolean = true;
51
52 export const enableCPUSuspense: boolean = true;
52 -export const enableUseEffectEventHook: boolean = true;
53 export const enableMoveBefore: boolean = false;
54 export const disableInputAttributeSyncing: boolean = false;
55 export const enableLegacyFBSupport: boolean = true;