Skip to content

Commit 3517bd9

Browse files
authored
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.
1 parent abd7bcd commit 3517bd9

8 files changed

+257
-117
lines changed

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
UpdatePayload,
1717
} from './ReactFiberHostConfig';
1818
import type {Fiber} from './ReactInternalTypes';
19-
import type {FiberRoot} from './ReactInternalTypes';
19+
import type {FiberRoot, EventFunctionWrapper} from './ReactInternalTypes';
2020
import type {Lanes} from './ReactFiberLane.new';
2121
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
2222
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
@@ -164,7 +164,6 @@ import {
164164
Layout as HookLayout,
165165
Insertion as HookInsertion,
166166
Passive as HookPassive,
167-
Snapshot as HookSnapshot,
168167
} from './ReactHookEffectTags';
169168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
170169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -416,8 +415,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
416415
case FunctionComponent: {
417416
if (enableUseEventHook) {
418417
if ((flags & Update) !== NoFlags) {
419-
// useEvent doesn't need to be cleaned up
420-
commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
418+
commitUseEventMount(finishedWork);
421419
}
422420
}
423421
break;
@@ -665,6 +663,21 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
665663
}
666664
}
667665

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+
668681
export function commitPassiveEffectDurations(
669682
finishedRoot: FiberRoot,
670683
finishedWork: Fiber,

packages/react-reconciler/src/ReactFiberCommitWork.old.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
UpdatePayload,
1717
} from './ReactFiberHostConfig';
1818
import type {Fiber} from './ReactInternalTypes';
19-
import type {FiberRoot} from './ReactInternalTypes';
19+
import type {FiberRoot, EventFunctionWrapper} from './ReactInternalTypes';
2020
import type {Lanes} from './ReactFiberLane.old';
2121
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
2222
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
@@ -164,7 +164,6 @@ import {
164164
Layout as HookLayout,
165165
Insertion as HookInsertion,
166166
Passive as HookPassive,
167-
Snapshot as HookSnapshot,
168167
} from './ReactHookEffectTags';
169168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
170169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -416,8 +415,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
416415
case FunctionComponent: {
417416
if (enableUseEventHook) {
418417
if ((flags & Update) !== NoFlags) {
419-
// useEvent doesn't need to be cleaned up
420-
commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
418+
commitUseEventMount(finishedWork);
421419
}
422420
}
423421
break;
@@ -665,6 +663,21 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
665663
}
666664
}
667665

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+
668681
export function commitPassiveEffectDurations(
669682
finishedRoot: FiberRoot,
670683
finishedWork: Fiber,

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
Dispatcher,
2222
HookType,
2323
MemoCache,
24+
EventFunctionWrapper,
2425
} from './ReactInternalTypes';
2526
import type {Lanes, Lane} from './ReactFiberLane.new';
2627
import type {HookFlags} from './ReactHookEffectTags';
@@ -86,7 +87,6 @@ import {
8687
Layout as HookLayout,
8788
Passive as HookPassive,
8889
Insertion as HookInsertion,
89-
Snapshot as HookSnapshot,
9090
} from './ReactHookEffectTags';
9191
import {
9292
getWorkInProgressRoot,
@@ -184,6 +184,7 @@ type StoreConsistencyCheck<T> = {
184184

185185
export type FunctionComponentUpdateQueue = {
186186
lastEffect: Effect | null,
187+
events: Array<() => mixed> | null,
187188
stores: Array<StoreConsistencyCheck<any>> | null,
188189
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
189190
memoCache?: MemoCache | null,
@@ -727,6 +728,7 @@ if (enableUseMemoCacheHook) {
727728
createFunctionComponentUpdateQueue = () => {
728729
return {
729730
lastEffect: null,
731+
events: null,
730732
stores: null,
731733
memoCache: null,
732734
};
@@ -735,6 +737,7 @@ if (enableUseMemoCacheHook) {
735737
createFunctionComponentUpdateQueue = () => {
736738
return {
737739
lastEffect: null,
740+
events: null,
738741
stores: null,
739742
};
740743
};
@@ -1871,49 +1874,52 @@ function updateEffect(
18711874
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
18721875
}
18731876

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+
}
18771896

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() {
18791902
if (isInvalidExecutionContextForEventFunction()) {
18801903
throw new Error(
18811904
"A function wrapped in useEvent can't be called during rendering.",
18821905
);
18831906
}
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;
18991910

1900-
return event;
1911+
useEventImpl(eventFn, callback);
1912+
hook.memoizedState = eventFn;
1913+
return eventFn;
19011914
}
19021915

1903-
function updateEvent<T>(callback: () => T): () => T {
1916+
function updateEvent<Args, Return, F: (...Array<Args>) => Return>(
1917+
callback: F,
1918+
): EventFunctionWrapper<Args, Return, F> {
19041919
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;
19171923
}
19181924

19191925
function mountInsertionEffect(
@@ -2890,9 +2896,11 @@ if (__DEV__) {
28902896
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
28912897
}
28922898
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> {
28962904
currentHookNameInDev = 'useEvent';
28972905
mountHookTypesDev();
28982906
return mountEvent(callback);
@@ -3048,8 +3056,10 @@ if (__DEV__) {
30483056
}
30493057
if (enableUseEventHook) {
30503058
(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> {
30533063
currentHookNameInDev = 'useEvent';
30543064
updateHookTypesDev();
30553065
return mountEvent(callback);
@@ -3204,9 +3214,11 @@ if (__DEV__) {
32043214
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
32053215
}
32063216
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> {
32103222
currentHookNameInDev = 'useEvent';
32113223
updateHookTypesDev();
32123224
return updateEvent(callback);
@@ -3363,8 +3375,10 @@ if (__DEV__) {
33633375
}
33643376
if (enableUseEventHook) {
33653377
(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> {
33683382
currentHookNameInDev = 'useEvent';
33693383
updateHookTypesDev();
33703384
return updateEvent(callback);
@@ -3547,8 +3561,10 @@ if (__DEV__) {
35473561
}
35483562
if (enableUseEventHook) {
35493563
(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> {
35523568
currentHookNameInDev = 'useEvent';
35533569
warnInvalidHookAccess();
35543570
mountHookTypesDev();
@@ -3732,8 +3748,10 @@ if (__DEV__) {
37323748
}
37333749
if (enableUseEventHook) {
37343750
(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> {
37373755
currentHookNameInDev = 'useEvent';
37383756
warnInvalidHookAccess();
37393757
updateHookTypesDev();
@@ -3918,8 +3936,10 @@ if (__DEV__) {
39183936
}
39193937
if (enableUseEventHook) {
39203938
(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> {
39233943
currentHookNameInDev = 'useEvent';
39243944
warnInvalidHookAccess();
39253945
updateHookTypesDev();

0 commit comments

Comments
 (0)