Refactor useEvent (#25336)
* Refactor useEvent Previously, the useEvent implementation made use of effect infra under the hood. This was a lot of extra overhead for functionality we didn't use (events have no deps, and no clean up functions). This PR refactors the implementation to instead use a queue to ensure that the callback is stable across renders. Additionally, the function signature was updated to infer the callback's argument types and return value. While this doesn't affect anything internal it more accurately describes what's being passed.
Lauren Tan committed
Sep 29, 2022 at 13:45 UTC
3517bd9f77dc63189f3bafedf83ba1bf8ae359df
8 files changed
+257
-117
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+17
-4
@@ -16,7 +16,7 @@ import type {
16
UpdatePayload,
17
} from './ReactFiberHostConfig';
18
import type {Fiber} from './ReactInternalTypes';
19
-import type {FiberRoot} from './ReactInternalTypes';
19
+import type {FiberRoot, EventFunctionWrapper} from './ReactInternalTypes';
20
import type {Lanes} from './ReactFiberLane.new';
21
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
22
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
@@ -164,7 +164,6 @@ import {
164
Layout as HookLayout,
165
Insertion as HookInsertion,
166
Passive as HookPassive,
167
- Snapshot as HookSnapshot,
167
} from './ReactHookEffectTags';
168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -416,8 +415,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
415
case FunctionComponent: {
416
if (enableUseEventHook) {
417
if ((flags & Update) !== NoFlags) {
419
- // useEvent doesn't need to be cleaned up
420
- commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
418
+ commitUseEventMount(finishedWork);
419
}
420
}
421
break;
@@ -665,6 +663,21 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
663
}
664
}
665
666
+function commitUseEventMount(finishedWork: Fiber) {
667
+ const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
668
+ const eventPayloads = updateQueue !== null ? updateQueue.events : null;
669
+ if (eventPayloads !== null) {
670
+ // FunctionComponentUpdateQueue.events is a flat array of
671
+ // [EventFunctionWrapper, EventFunction, ...], so increment by 2 each iteration to find the next
672
+ // pair.
673
+ for (let ii = 0; ii < eventPayloads.length; ii += 2) {
674
+ const eventFn: EventFunctionWrapper<any, any, any> = eventPayloads[ii];
675
+ const nextImpl = eventPayloads[ii + 1];
676
+ eventFn._impl = nextImpl;
677
+ }
678
+ }
679
+}
680
+
681
export function commitPassiveEffectDurations(
682
finishedRoot: FiberRoot,
683
finishedWork: Fiber,
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+17
-4
@@ -16,7 +16,7 @@ import type {
16
UpdatePayload,
17
} from './ReactFiberHostConfig';
18
import type {Fiber} from './ReactInternalTypes';
19
-import type {FiberRoot} from './ReactInternalTypes';
19
+import type {FiberRoot, EventFunctionWrapper} from './ReactInternalTypes';
20
import type {Lanes} from './ReactFiberLane.old';
21
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
22
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
@@ -164,7 +164,6 @@ import {
164
Layout as HookLayout,
165
Insertion as HookInsertion,
166
Passive as HookPassive,
167
- Snapshot as HookSnapshot,
167
} from './ReactHookEffectTags';
168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -416,8 +415,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
415
case FunctionComponent: {
416
if (enableUseEventHook) {
417
if ((flags & Update) !== NoFlags) {
419
- // useEvent doesn't need to be cleaned up
420
- commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
418
+ commitUseEventMount(finishedWork);
419
}
420
}
421
break;
@@ -665,6 +663,21 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
663
}
664
}
665
666
+function commitUseEventMount(finishedWork: Fiber) {
667
+ const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
668
+ const eventPayloads = updateQueue !== null ? updateQueue.events : null;
669
+ if (eventPayloads !== null) {
670
+ // FunctionComponentUpdateQueue.events is a flat array of
671
+ // [EventFunctionWrapper, EventFunction, ...], so increment by 2 each iteration to find the next
672
+ // pair.
673
+ for (let ii = 0; ii < eventPayloads.length; ii += 2) {
674
+ const eventFn: EventFunctionWrapper<any, any, any> = eventPayloads[ii];
675
+ const nextImpl = eventPayloads[ii + 1];
676
+ eventFn._impl = nextImpl;
677
+ }
678
+ }
679
+}
680
+
681
export function commitPassiveEffectDurations(
682
finishedRoot: FiberRoot,
683
finishedWork: Fiber,
packages/react-reconciler/src/ReactFiberHooks.new.js
+70
-50
@@ -21,6 +21,7 @@ import type {
21
Dispatcher,
22
HookType,
23
MemoCache,
24
+ EventFunctionWrapper,
25
} from './ReactInternalTypes';
26
import type {Lanes, Lane} from './ReactFiberLane.new';
27
import type {HookFlags} from './ReactHookEffectTags';
@@ -86,7 +87,6 @@ import {
87
Layout as HookLayout,
88
Passive as HookPassive,
89
Insertion as HookInsertion,
89
- Snapshot as HookSnapshot,
90
} from './ReactHookEffectTags';
91
import {
92
getWorkInProgressRoot,
@@ -184,6 +184,7 @@ type StoreConsistencyCheck<T> = {
184
185
export type FunctionComponentUpdateQueue = {
186
lastEffect: Effect | null,
187
+ events: Array<() => mixed> | null,
188
stores: Array<StoreConsistencyCheck<any>> | null,
189
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
190
memoCache?: MemoCache | null,
@@ -727,6 +728,7 @@ if (enableUseMemoCacheHook) {
728
createFunctionComponentUpdateQueue = () => {
729
return {
730
lastEffect: null,
731
+ events: null,
732
stores: null,
733
memoCache: null,
734
};
@@ -735,6 +737,7 @@ if (enableUseMemoCacheHook) {
737
createFunctionComponentUpdateQueue = () => {
738
return {
739
lastEffect: null,
740
+ events: null,
741
stores: null,
742
};
743
};
@@ -1871,49 +1874,52 @@ function updateEffect(
1874
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1875
}
1876
1874
-function mountEvent<T>(callback: () => T): () => T {
1875
- const hook = mountWorkInProgressHook();
1876
- const ref = {current: callback};
1877
+function useEventImpl<Args, Return, F: (...Array<Args>) => Return>(
1878
+ event: EventFunctionWrapper<Args, Return, F>,
1879
+ nextImpl: F,
1880
+) {
1881
+ currentlyRenderingFiber.flags |= UpdateEffect;
1882
+ let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
1883
+ if (componentUpdateQueue === null) {
1884
+ componentUpdateQueue = createFunctionComponentUpdateQueue();
1885
+ currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
1886
+ componentUpdateQueue.events = [event, nextImpl];
1887
+ } else {
1888
+ const events = componentUpdateQueue.events;
1889
+ if (events === null) {
1890
+ componentUpdateQueue.events = [event, nextImpl];
1891
+ } else {
1892
+ events.push(event, nextImpl);
1893
+ }
1894
+ }
1895
+}
1896
1878
- function event() {
1897
+function mountEvent<Args, Return, F: (...Array<Args>) => Return>(
1898
+ callback: F,
1899
+): EventFunctionWrapper<Args, Return, F> {
1900
+ const hook = mountWorkInProgressHook();
1901
+ const eventFn: EventFunctionWrapper<Args, Return, F> = function eventFn() {
1902
if (isInvalidExecutionContextForEventFunction()) {
1903
throw new Error(
1904
"A function wrapped in useEvent can't be called during rendering.",
1905
);
1906
}
1884
- return ref.current.apply(undefined, arguments);
1885
- }
1886
-
1887
- // TODO: We don't need all the overhead of an effect object since there are no deps and no
1888
- // clean up functions.
1889
- mountEffectImpl(
1890
- UpdateEffect,
1891
- HookSnapshot,
1892
- () => {
1893
- ref.current = callback;
1894
- },
1895
- [ref, callback],
1896
- );
1897
-
1898
- hook.memoizedState = [ref, event];
1907
+ return eventFn._impl.apply(undefined, arguments);
1908
+ };
1909
+ eventFn._impl = callback;
1910
1900
- return event;
1911
+ useEventImpl(eventFn, callback);
1912
+ hook.memoizedState = eventFn;
1913
+ return eventFn;
1914
}
1915
1903
-function updateEvent<T>(callback: () => T): () => T {
1916
+function updateEvent<Args, Return, F: (...Array<Args>) => Return>(
1917
+ callback: F,
1918
+): EventFunctionWrapper<Args, Return, F> {
1919
const hook = updateWorkInProgressHook();
1905
- const ref = hook.memoizedState[0];
1906
-
1907
- updateEffectImpl(
1908
- UpdateEffect,
1909
- HookSnapshot,
1910
- () => {
1911
- ref.current = callback;
1912
- },
1913
- [ref, callback],
1914
- );
1915
-
1916
- return hook.memoizedState[1];
1920
+ const eventFn = hook.memoizedState;
1921
+ useEventImpl(eventFn, callback);
1922
+ return eventFn;
1923
}
1924
1925
function mountInsertionEffect(
@@ -2890,9 +2896,11 @@ if (__DEV__) {
2896
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
2897
}
2898
if (enableUseEventHook) {
2893
- (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<T>(
2894
- callback: () => T,
2895
- ): () => T {
2899
+ (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
2900
+ Args,
2901
+ Return,
2902
+ F: (...Array<Args>) => Return,
2903
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
2904
currentHookNameInDev = 'useEvent';
2905
mountHookTypesDev();
2906
return mountEvent(callback);
@@ -3048,8 +3056,10 @@ if (__DEV__) {
3056
}
3057
if (enableUseEventHook) {
3058
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent<
3051
- T,
3052
- >(callback: () => T): () => T {
3059
+ Args,
3060
+ Return,
3061
+ F: (...Array<Args>) => Return,
3062
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3063
currentHookNameInDev = 'useEvent';
3064
updateHookTypesDev();
3065
return mountEvent(callback);
@@ -3204,9 +3214,11 @@ if (__DEV__) {
3214
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
3215
}
3216
if (enableUseEventHook) {
3207
- (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<T>(
3208
- callback: () => T,
3209
- ): () => T {
3217
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
3218
+ Args,
3219
+ Return,
3220
+ F: (...Array<Args>) => Return,
3221
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3222
currentHookNameInDev = 'useEvent';
3223
updateHookTypesDev();
3224
return updateEvent(callback);
@@ -3363,8 +3375,10 @@ if (__DEV__) {
3375
}
3376
if (enableUseEventHook) {
3377
(HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3366
- T,
3367
- >(callback: () => T): () => T {
3378
+ Args,
3379
+ Return,
3380
+ F: (...Array<Args>) => Return,
3381
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3382
currentHookNameInDev = 'useEvent';
3383
updateHookTypesDev();
3384
return updateEvent(callback);
@@ -3547,8 +3561,10 @@ if (__DEV__) {
3561
}
3562
if (enableUseEventHook) {
3563
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
3550
- T,
3551
- >(callback: () => T): () => T {
3564
+ Args,
3565
+ Return,
3566
+ F: (...Array<Args>) => Return,
3567
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3568
currentHookNameInDev = 'useEvent';
3569
warnInvalidHookAccess();
3570
mountHookTypesDev();
@@ -3732,8 +3748,10 @@ if (__DEV__) {
3748
}
3749
if (enableUseEventHook) {
3750
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
3735
- T,
3736
- >(callback: () => T): () => T {
3751
+ Args,
3752
+ Return,
3753
+ F: (...Array<Args>) => Return,
3754
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3755
currentHookNameInDev = 'useEvent';
3756
warnInvalidHookAccess();
3757
updateHookTypesDev();
@@ -3918,8 +3936,10 @@ if (__DEV__) {
3936
}
3937
if (enableUseEventHook) {
3938
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3921
- T,
3922
- >(callback: () => T): () => T {
3939
+ Args,
3940
+ Return,
3941
+ F: (...Array<Args>) => Return,
3942
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3943
currentHookNameInDev = 'useEvent';
3944
warnInvalidHookAccess();
3945
updateHookTypesDev();
packages/react-reconciler/src/ReactFiberHooks.old.js
+70
-50
@@ -21,6 +21,7 @@ import type {
21
Dispatcher,
22
HookType,
23
MemoCache,
24
+ EventFunctionWrapper,
25
} from './ReactInternalTypes';
26
import type {Lanes, Lane} from './ReactFiberLane.old';
27
import type {HookFlags} from './ReactHookEffectTags';
@@ -86,7 +87,6 @@ import {
87
Layout as HookLayout,
88
Passive as HookPassive,
89
Insertion as HookInsertion,
89
- Snapshot as HookSnapshot,
90
} from './ReactHookEffectTags';
91
import {
92
getWorkInProgressRoot,
@@ -184,6 +184,7 @@ type StoreConsistencyCheck<T> = {
184
185
export type FunctionComponentUpdateQueue = {
186
lastEffect: Effect | null,
187
+ events: Array<() => mixed> | null,
188
stores: Array<StoreConsistencyCheck<any>> | null,
189
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
190
memoCache?: MemoCache | null,
@@ -727,6 +728,7 @@ if (enableUseMemoCacheHook) {
728
createFunctionComponentUpdateQueue = () => {
729
return {
730
lastEffect: null,
731
+ events: null,
732
stores: null,
733
memoCache: null,
734
};
@@ -735,6 +737,7 @@ if (enableUseMemoCacheHook) {
737
createFunctionComponentUpdateQueue = () => {
738
return {
739
lastEffect: null,
740
+ events: null,
741
stores: null,
742
};
743
};
@@ -1871,49 +1874,52 @@ function updateEffect(
1874
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1875
}
1876
1874
-function mountEvent<T>(callback: () => T): () => T {
1875
- const hook = mountWorkInProgressHook();
1876
- const ref = {current: callback};
1877
+function useEventImpl<Args, Return, F: (...Array<Args>) => Return>(
1878
+ event: EventFunctionWrapper<Args, Return, F>,
1879
+ nextImpl: F,
1880
+) {
1881
+ currentlyRenderingFiber.flags |= UpdateEffect;
1882
+ let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
1883
+ if (componentUpdateQueue === null) {
1884
+ componentUpdateQueue = createFunctionComponentUpdateQueue();
1885
+ currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
1886
+ componentUpdateQueue.events = [event, nextImpl];
1887
+ } else {
1888
+ const events = componentUpdateQueue.events;
1889
+ if (events === null) {
1890
+ componentUpdateQueue.events = [event, nextImpl];
1891
+ } else {
1892
+ events.push(event, nextImpl);
1893
+ }
1894
+ }
1895
+}
1896
1878
- function event() {
1897
+function mountEvent<Args, Return, F: (...Array<Args>) => Return>(
1898
+ callback: F,
1899
+): EventFunctionWrapper<Args, Return, F> {
1900
+ const hook = mountWorkInProgressHook();
1901
+ const eventFn: EventFunctionWrapper<Args, Return, F> = function eventFn() {
1902
if (isInvalidExecutionContextForEventFunction()) {
1903
throw new Error(
1904
"A function wrapped in useEvent can't be called during rendering.",
1905
);
1906
}
1884
- return ref.current.apply(undefined, arguments);
1885
- }
1886
-
1887
- // TODO: We don't need all the overhead of an effect object since there are no deps and no
1888
- // clean up functions.
1889
- mountEffectImpl(
1890
- UpdateEffect,
1891
- HookSnapshot,
1892
- () => {
1893
- ref.current = callback;
1894
- },
1895
- [ref, callback],
1896
- );
1897
-
1898
- hook.memoizedState = [ref, event];
1907
+ return eventFn._impl.apply(undefined, arguments);
1908
+ };
1909
+ eventFn._impl = callback;
1910
1900
- return event;
1911
+ useEventImpl(eventFn, callback);
1912
+ hook.memoizedState = eventFn;
1913
+ return eventFn;
1914
}
1915
1903
-function updateEvent<T>(callback: () => T): () => T {
1916
+function updateEvent<Args, Return, F: (...Array<Args>) => Return>(
1917
+ callback: F,
1918
+): EventFunctionWrapper<Args, Return, F> {
1919
const hook = updateWorkInProgressHook();
1905
- const ref = hook.memoizedState[0];
1906
-
1907
- updateEffectImpl(
1908
- UpdateEffect,
1909
- HookSnapshot,
1910
- () => {
1911
- ref.current = callback;
1912
- },
1913
- [ref, callback],
1914
- );
1915
-
1916
- return hook.memoizedState[1];
1920
+ const eventFn = hook.memoizedState;
1921
+ useEventImpl(eventFn, callback);
1922
+ return eventFn;
1923
}
1924
1925
function mountInsertionEffect(
@@ -2890,9 +2896,11 @@ if (__DEV__) {
2896
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
2897
}
2898
if (enableUseEventHook) {
2893
- (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<T>(
2894
- callback: () => T,
2895
- ): () => T {
2899
+ (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
2900
+ Args,
2901
+ Return,
2902
+ F: (...Array<Args>) => Return,
2903
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
2904
currentHookNameInDev = 'useEvent';
2905
mountHookTypesDev();
2906
return mountEvent(callback);
@@ -3048,8 +3056,10 @@ if (__DEV__) {
3056
}
3057
if (enableUseEventHook) {
3058
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent<
3051
- T,
3052
- >(callback: () => T): () => T {
3059
+ Args,
3060
+ Return,
3061
+ F: (...Array<Args>) => Return,
3062
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3063
currentHookNameInDev = 'useEvent';
3064
updateHookTypesDev();
3065
return mountEvent(callback);
@@ -3204,9 +3214,11 @@ if (__DEV__) {
3214
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
3215
}
3216
if (enableUseEventHook) {
3207
- (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<T>(
3208
- callback: () => T,
3209
- ): () => T {
3217
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
3218
+ Args,
3219
+ Return,
3220
+ F: (...Array<Args>) => Return,
3221
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3222
currentHookNameInDev = 'useEvent';
3223
updateHookTypesDev();
3224
return updateEvent(callback);
@@ -3363,8 +3375,10 @@ if (__DEV__) {
3375
}
3376
if (enableUseEventHook) {
3377
(HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3366
- T,
3367
- >(callback: () => T): () => T {
3378
+ Args,
3379
+ Return,
3380
+ F: (...Array<Args>) => Return,
3381
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3382
currentHookNameInDev = 'useEvent';
3383
updateHookTypesDev();
3384
return updateEvent(callback);
@@ -3547,8 +3561,10 @@ if (__DEV__) {
3561
}
3562
if (enableUseEventHook) {
3563
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
3550
- T,
3551
- >(callback: () => T): () => T {
3564
+ Args,
3565
+ Return,
3566
+ F: (...Array<Args>) => Return,
3567
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3568
currentHookNameInDev = 'useEvent';
3569
warnInvalidHookAccess();
3570
mountHookTypesDev();
@@ -3732,8 +3748,10 @@ if (__DEV__) {
3748
}
3749
if (enableUseEventHook) {
3750
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
3735
- T,
3736
- >(callback: () => T): () => T {
3751
+ Args,
3752
+ Return,
3753
+ F: (...Array<Args>) => Return,
3754
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3755
currentHookNameInDev = 'useEvent';
3756
warnInvalidHookAccess();
3757
updateHookTypesDev();
@@ -3918,8 +3936,10 @@ if (__DEV__) {
3936
}
3937
if (enableUseEventHook) {
3938
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3921
- T,
3922
- >(callback: () => T): () => T {
3939
+ Args,
3940
+ Return,
3941
+ F: (...Array<Args>) => Return,
3942
+ >(callback: F): EventFunctionWrapper<Args, Return, F> {
3943
currentHookNameInDev = 'useEvent';
3944
warnInvalidHookAccess();
3945
updateHookTypesDev();
packages/react-reconciler/src/ReactHookEffectTags.js
+5
-6
@@ -9,13 +9,12 @@
9
10
export type HookFlags = number;
11
12
-export const NoFlags = /* */ 0b00000;
12
+export const NoFlags = /* */ 0b0000;
13
14
// Represents whether effect should fire.
15
-export const HasEffect = /* */ 0b00001;
15
+export const HasEffect = /* */ 0b0001;
16
17
// Represents the phase in which the effect (not the clean-up) fires.
18
-export const Snapshot = /* */ 0b00010;
19
-export const Insertion = /* */ 0b00100;
20
-export const Layout = /* */ 0b01000;
21
-export const Passive = /* */ 0b10000;
18
+export const Insertion = /* */ 0b0010;
19
+export const Layout = /* */ 0b0100;
20
+export const Passive = /* */ 0b1000;
packages/react-reconciler/src/ReactInternalTypes.js
+14
-1
@@ -286,6 +286,17 @@ type SuspenseCallbackOnlyFiberRootProperties = {
286
hydrationCallbacks: null | SuspenseHydrationCallbacks,
287
};
288
289
+// A wrapper callable object around a useEvent callback that throws if the callback is called during
290
+// rendering. The _impl property points to the actual implementation.
291
+export type EventFunctionWrapper<
292
+ Args,
293
+ Return,
294
+ F: (...Array<Args>) => Return,
295
+> = {
296
+ (): F,
297
+ _impl: F,
298
+};
299
+
300
export type TransitionTracingCallbacks = {
301
onTransitionStart?: (transitionName: string, startTime: number) => void,
302
onTransitionProgress?: (
@@ -377,7 +388,9 @@ export type Dispatcher = {
388
create: () => (() => void) | void,
389
deps: Array<mixed> | void | null,
390
): void,
380
- useEvent?: <T>(callback: () => T) => () => T,
391
+ useEvent?: <Args, Return, F: (...Array<Args>) => Return>(
392
+ callback: F,
393
+ ) => EventFunctionWrapper<Args, Return, F>,
394
useInsertionEffect(
395
create: () => (() => void) | void,
396
deps: Array<mixed> | void | null,
packages/react-reconciler/src/__tests__/useEvent-test.js
+56
@@ -117,6 +117,62 @@ describe('useEvent', () => {
117
]);
118
});
119
120
+ // @gate enableUseEventHook
121
+ it('can be defined more than once', () => {
122
+ class IncrementButton extends React.PureComponent {
123
+ increment = () => {
124
+ this.props.onClick();
125
+ };
126
+ multiply = () => {
127
+ this.props.onMouseEnter();
128
+ };
129
+ render() {
130
+ return <Text text="Increment" />;
131
+ }
132
+ }
133
+
134
+ function Counter({incrementBy}) {
135
+ const [count, updateCount] = useState(0);
136
+ const onClick = useEvent(() => updateCount(c => c + incrementBy));
137
+ const onMouseEnter = useEvent(() => {
138
+ updateCount(c => c * incrementBy);
139
+ });
140
+
141
+ return (
142
+ <>
143
+ <IncrementButton
144
+ onClick={() => onClick()}
145
+ onMouseEnter={() => onMouseEnter()}
146
+ ref={button}
147
+ />
148
+ <Text text={'Count: ' + count} />
149
+ </>
150
+ );
151
+ }
152
+
153
+ const button = React.createRef(null);
154
+ ReactNoop.render(<Counter incrementBy={5} />);
155
+ expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
156
+ expect(ReactNoop.getChildren()).toEqual([
157
+ span('Increment'),
158
+ span('Count: 0'),
159
+ ]);
160
+
161
+ act(button.current.increment);
162
+ expect(Scheduler).toHaveYielded(['Increment', 'Count: 5']);
163
+ expect(ReactNoop.getChildren()).toEqual([
164
+ span('Increment'),
165
+ span('Count: 5'),
166
+ ]);
167
+
168
+ act(button.current.multiply);
169
+ expect(Scheduler).toHaveYielded(['Increment', 'Count: 25']);
170
+ expect(ReactNoop.getChildren()).toEqual([
171
+ span('Increment'),
172
+ span('Count: 25'),
173
+ ]);
174
+ });
175
+
176
// @gate enableUseEventHook
177
it('does not preserve `this` in event functions', () => {
178
class GreetButton extends React.PureComponent {
packages/react-server/src/ReactFizzHooks.js
+8
-2
@@ -7,7 +7,10 @@
7
* @flow
8
*/
9
10
-import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
10
+import type {
11
+ Dispatcher as DispatcherType,
12
+ EventFunctionWrapper,
13
+} from 'react-reconciler/src/ReactInternalTypes';
14
15
import type {
16
MutableSource,
@@ -509,7 +512,10 @@ function throwOnUseEventCall() {
512
);
513
}
514
512
-export function useEvent<T>(callback: () => T): () => T {
515
+export function useEvent<Args, Return, F: (...Array<Args>) => Return>(
516
+ callback: F,
517
+): EventFunctionWrapper<Args, Return, F> {
518
+ // $FlowIgnore[incompatible-return] useEvent doesn't work in Fizz
519
return throwOnUseEventCall;
520
}
521