[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;