Skip to content

Commit 3cc792b

Browse files
potetoacdlite
andauthored
[useEvent] Non-stable function identity (#25473)
* [useEvent] Non-stable function identity Since useEvent shouldn't go in the dependency list of whatever is consuming it (which is enforced by the fact that useEvent functions are always locally created and never passed by reference), its identity doesn't matter. Effectively, this PR is a runtime assertion that you can't rely on the return value of useEvent to be stable. * Test: Events should see latest bindings The key feature of useEvent that makes it different from useCallback is that events always see the latest committed values. There's no such thing as a "stale" event handler. * Don't queue a commit effect on mount * Inline event function wrapping - Inlines wrapping of the callback - Use a mutable ref-style object instead of a callable object - Fix types Co-authored-by: Andrew Clark <git@andrewclark.io>
1 parent 9872928 commit 3cc792b

7 files changed

+179
-99
lines changed

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

+4-12
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ import type {
1515
ChildSet,
1616
UpdatePayload,
1717
} from './ReactFiberHostConfig';
18-
import type {
19-
Fiber,
20-
FiberRoot,
21-
EventFunctionWrapper,
22-
} from './ReactInternalTypes';
18+
import type {Fiber, FiberRoot} from './ReactInternalTypes';
2319
import type {Lanes} from './ReactFiberLane.new';
2420
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
2521
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
@@ -689,13 +685,9 @@ function commitUseEventMount(finishedWork: Fiber) {
689685
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
690686
const eventPayloads = updateQueue !== null ? updateQueue.events : null;
691687
if (eventPayloads !== null) {
692-
// FunctionComponentUpdateQueue.events is a flat array of
693-
// [EventFunctionWrapper, EventFunction, ...], so increment by 2 each iteration to find the next
694-
// pair.
695-
for (let ii = 0; ii < eventPayloads.length; ii += 2) {
696-
const eventFn: EventFunctionWrapper<any, any, any> = eventPayloads[ii];
697-
const nextImpl = eventPayloads[ii + 1];
698-
eventFn._impl = nextImpl;
688+
for (let ii = 0; ii < eventPayloads.length; ii++) {
689+
const {ref, nextImpl} = eventPayloads[ii];
690+
ref.impl = nextImpl;
699691
}
700692
}
701693
}

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

+4-12
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ import type {
1515
ChildSet,
1616
UpdatePayload,
1717
} from './ReactFiberHostConfig';
18-
import type {
19-
Fiber,
20-
FiberRoot,
21-
EventFunctionWrapper,
22-
} from './ReactInternalTypes';
18+
import type {Fiber, FiberRoot} from './ReactInternalTypes';
2319
import type {Lanes} from './ReactFiberLane.old';
2420
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
2521
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
@@ -689,13 +685,9 @@ function commitUseEventMount(finishedWork: Fiber) {
689685
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
690686
const eventPayloads = updateQueue !== null ? updateQueue.events : null;
691687
if (eventPayloads !== null) {
692-
// FunctionComponentUpdateQueue.events is a flat array of
693-
// [EventFunctionWrapper, EventFunction, ...], so increment by 2 each iteration to find the next
694-
// pair.
695-
for (let ii = 0; ii < eventPayloads.length; ii += 2) {
696-
const eventFn: EventFunctionWrapper<any, any, any> = eventPayloads[ii];
697-
const nextImpl = eventPayloads[ii + 1];
698-
eventFn._impl = nextImpl;
688+
for (let ii = 0; ii < eventPayloads.length; ii++) {
689+
const {ref, nextImpl} = eventPayloads[ii];
690+
ref.impl = nextImpl;
699691
}
700692
}
701693
}

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

+38-27
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type {
2222
Dispatcher,
2323
HookType,
2424
MemoCache,
25-
EventFunctionWrapper,
2625
} from './ReactInternalTypes';
2726
import type {Lanes, Lane} from './ReactFiberLane.new';
2827
import type {HookFlags} from './ReactHookEffectTags';
@@ -189,9 +188,17 @@ type StoreConsistencyCheck<T> = {
189188
getSnapshot: () => T,
190189
};
191190

191+
type EventFunctionPayload<Args, Return, F: (...Array<Args>) => Return> = {
192+
ref: {
193+
eventFn: F,
194+
impl: F,
195+
},
196+
nextImpl: F,
197+
};
198+
192199
export type FunctionComponentUpdateQueue = {
193200
lastEffect: Effect | null,
194-
events: Array<() => mixed> | null,
201+
events: Array<EventFunctionPayload<any, any, any>> | null,
195202
stores: Array<StoreConsistencyCheck<any>> | null,
196203
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
197204
memoCache?: MemoCache | null,
@@ -1909,52 +1916,56 @@ function updateEffect(
19091916
}
19101917

19111918
function useEventImpl<Args, Return, F: (...Array<Args>) => Return>(
1912-
event: EventFunctionWrapper<Args, Return, F>,
1913-
nextImpl: F,
1919+
payload: EventFunctionPayload<Args, Return, F>,
19141920
) {
19151921
currentlyRenderingFiber.flags |= UpdateEffect;
19161922
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
19171923
if (componentUpdateQueue === null) {
19181924
componentUpdateQueue = createFunctionComponentUpdateQueue();
19191925
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
1920-
componentUpdateQueue.events = [event, nextImpl];
1926+
componentUpdateQueue.events = [payload];
19211927
} else {
19221928
const events = componentUpdateQueue.events;
19231929
if (events === null) {
1924-
componentUpdateQueue.events = [event, nextImpl];
1930+
componentUpdateQueue.events = [payload];
19251931
} else {
1926-
events.push(event, nextImpl);
1932+
events.push(payload);
19271933
}
19281934
}
19291935
}
19301936

19311937
function mountEvent<Args, Return, F: (...Array<Args>) => Return>(
19321938
callback: F,
1933-
): EventFunctionWrapper<Args, Return, F> {
1939+
): F {
19341940
const hook = mountWorkInProgressHook();
1935-
const eventFn: EventFunctionWrapper<Args, Return, F> = function eventFn() {
1941+
const ref = {impl: callback};
1942+
hook.memoizedState = ref;
1943+
// $FlowIgnore[incompatible-return]
1944+
return function eventFn() {
19361945
if (isInvalidExecutionContextForEventFunction()) {
19371946
throw new Error(
19381947
"A function wrapped in useEvent can't be called during rendering.",
19391948
);
19401949
}
1941-
// $FlowFixMe[prop-missing] found when upgrading Flow
1942-
return eventFn._impl.apply(undefined, arguments);
1950+
return ref.impl.apply(undefined, arguments);
19431951
};
1944-
eventFn._impl = callback;
1945-
1946-
useEventImpl(eventFn, callback);
1947-
hook.memoizedState = eventFn;
1948-
return eventFn;
19491952
}
19501953

19511954
function updateEvent<Args, Return, F: (...Array<Args>) => Return>(
19521955
callback: F,
1953-
): EventFunctionWrapper<Args, Return, F> {
1956+
): F {
19541957
const hook = updateWorkInProgressHook();
1955-
const eventFn = hook.memoizedState;
1956-
useEventImpl(eventFn, callback);
1957-
return eventFn;
1958+
const ref = hook.memoizedState;
1959+
useEventImpl({ref, nextImpl: callback});
1960+
// $FlowIgnore[incompatible-return]
1961+
return function eventFn() {
1962+
if (isInvalidExecutionContextForEventFunction()) {
1963+
throw new Error(
1964+
"A function wrapped in useEvent can't be called during rendering.",
1965+
);
1966+
}
1967+
return ref.impl.apply(undefined, arguments);
1968+
};
19581969
}
19591970

19601971
function mountInsertionEffect(
@@ -2916,7 +2927,7 @@ if (__DEV__) {
29162927
Args,
29172928
Return,
29182929
F: (...Array<Args>) => Return,
2919-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
2930+
>(callback: F): F {
29202931
currentHookNameInDev = 'useEvent';
29212932
mountHookTypesDev();
29222933
return mountEvent(callback);
@@ -3073,7 +3084,7 @@ if (__DEV__) {
30733084
Args,
30743085
Return,
30753086
F: (...Array<Args>) => Return,
3076-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3087+
>(callback: F): F {
30773088
currentHookNameInDev = 'useEvent';
30783089
updateHookTypesDev();
30793090
return mountEvent(callback);
@@ -3230,7 +3241,7 @@ if (__DEV__) {
32303241
Args,
32313242
Return,
32323243
F: (...Array<Args>) => Return,
3233-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3244+
>(callback: F): F {
32343245
currentHookNameInDev = 'useEvent';
32353246
updateHookTypesDev();
32363247
return updateEvent(callback);
@@ -3388,7 +3399,7 @@ if (__DEV__) {
33883399
Args,
33893400
Return,
33903401
F: (...Array<Args>) => Return,
3391-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3402+
>(callback: F): F {
33923403
currentHookNameInDev = 'useEvent';
33933404
updateHookTypesDev();
33943405
return updateEvent(callback);
@@ -3572,7 +3583,7 @@ if (__DEV__) {
35723583
Args,
35733584
Return,
35743585
F: (...Array<Args>) => Return,
3575-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3586+
>(callback: F): F {
35763587
currentHookNameInDev = 'useEvent';
35773588
warnInvalidHookAccess();
35783589
mountHookTypesDev();
@@ -3757,7 +3768,7 @@ if (__DEV__) {
37573768
Args,
37583769
Return,
37593770
F: (...Array<Args>) => Return,
3760-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3771+
>(callback: F): F {
37613772
currentHookNameInDev = 'useEvent';
37623773
warnInvalidHookAccess();
37633774
updateHookTypesDev();
@@ -3943,7 +3954,7 @@ if (__DEV__) {
39433954
Args,
39443955
Return,
39453956
F: (...Array<Args>) => Return,
3946-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3957+
>(callback: F): F {
39473958
currentHookNameInDev = 'useEvent';
39483959
warnInvalidHookAccess();
39493960
updateHookTypesDev();

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

+38-27
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type {
2222
Dispatcher,
2323
HookType,
2424
MemoCache,
25-
EventFunctionWrapper,
2625
} from './ReactInternalTypes';
2726
import type {Lanes, Lane} from './ReactFiberLane.old';
2827
import type {HookFlags} from './ReactHookEffectTags';
@@ -189,9 +188,17 @@ type StoreConsistencyCheck<T> = {
189188
getSnapshot: () => T,
190189
};
191190

191+
type EventFunctionPayload<Args, Return, F: (...Array<Args>) => Return> = {
192+
ref: {
193+
eventFn: F,
194+
impl: F,
195+
},
196+
nextImpl: F,
197+
};
198+
192199
export type FunctionComponentUpdateQueue = {
193200
lastEffect: Effect | null,
194-
events: Array<() => mixed> | null,
201+
events: Array<EventFunctionPayload<any, any, any>> | null,
195202
stores: Array<StoreConsistencyCheck<any>> | null,
196203
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
197204
memoCache?: MemoCache | null,
@@ -1909,52 +1916,56 @@ function updateEffect(
19091916
}
19101917

19111918
function useEventImpl<Args, Return, F: (...Array<Args>) => Return>(
1912-
event: EventFunctionWrapper<Args, Return, F>,
1913-
nextImpl: F,
1919+
payload: EventFunctionPayload<Args, Return, F>,
19141920
) {
19151921
currentlyRenderingFiber.flags |= UpdateEffect;
19161922
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
19171923
if (componentUpdateQueue === null) {
19181924
componentUpdateQueue = createFunctionComponentUpdateQueue();
19191925
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
1920-
componentUpdateQueue.events = [event, nextImpl];
1926+
componentUpdateQueue.events = [payload];
19211927
} else {
19221928
const events = componentUpdateQueue.events;
19231929
if (events === null) {
1924-
componentUpdateQueue.events = [event, nextImpl];
1930+
componentUpdateQueue.events = [payload];
19251931
} else {
1926-
events.push(event, nextImpl);
1932+
events.push(payload);
19271933
}
19281934
}
19291935
}
19301936

19311937
function mountEvent<Args, Return, F: (...Array<Args>) => Return>(
19321938
callback: F,
1933-
): EventFunctionWrapper<Args, Return, F> {
1939+
): F {
19341940
const hook = mountWorkInProgressHook();
1935-
const eventFn: EventFunctionWrapper<Args, Return, F> = function eventFn() {
1941+
const ref = {impl: callback};
1942+
hook.memoizedState = ref;
1943+
// $FlowIgnore[incompatible-return]
1944+
return function eventFn() {
19361945
if (isInvalidExecutionContextForEventFunction()) {
19371946
throw new Error(
19381947
"A function wrapped in useEvent can't be called during rendering.",
19391948
);
19401949
}
1941-
// $FlowFixMe[prop-missing] found when upgrading Flow
1942-
return eventFn._impl.apply(undefined, arguments);
1950+
return ref.impl.apply(undefined, arguments);
19431951
};
1944-
eventFn._impl = callback;
1945-
1946-
useEventImpl(eventFn, callback);
1947-
hook.memoizedState = eventFn;
1948-
return eventFn;
19491952
}
19501953

19511954
function updateEvent<Args, Return, F: (...Array<Args>) => Return>(
19521955
callback: F,
1953-
): EventFunctionWrapper<Args, Return, F> {
1956+
): F {
19541957
const hook = updateWorkInProgressHook();
1955-
const eventFn = hook.memoizedState;
1956-
useEventImpl(eventFn, callback);
1957-
return eventFn;
1958+
const ref = hook.memoizedState;
1959+
useEventImpl({ref, nextImpl: callback});
1960+
// $FlowIgnore[incompatible-return]
1961+
return function eventFn() {
1962+
if (isInvalidExecutionContextForEventFunction()) {
1963+
throw new Error(
1964+
"A function wrapped in useEvent can't be called during rendering.",
1965+
);
1966+
}
1967+
return ref.impl.apply(undefined, arguments);
1968+
};
19581969
}
19591970

19601971
function mountInsertionEffect(
@@ -2916,7 +2927,7 @@ if (__DEV__) {
29162927
Args,
29172928
Return,
29182929
F: (...Array<Args>) => Return,
2919-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
2930+
>(callback: F): F {
29202931
currentHookNameInDev = 'useEvent';
29212932
mountHookTypesDev();
29222933
return mountEvent(callback);
@@ -3073,7 +3084,7 @@ if (__DEV__) {
30733084
Args,
30743085
Return,
30753086
F: (...Array<Args>) => Return,
3076-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3087+
>(callback: F): F {
30773088
currentHookNameInDev = 'useEvent';
30783089
updateHookTypesDev();
30793090
return mountEvent(callback);
@@ -3230,7 +3241,7 @@ if (__DEV__) {
32303241
Args,
32313242
Return,
32323243
F: (...Array<Args>) => Return,
3233-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3244+
>(callback: F): F {
32343245
currentHookNameInDev = 'useEvent';
32353246
updateHookTypesDev();
32363247
return updateEvent(callback);
@@ -3388,7 +3399,7 @@ if (__DEV__) {
33883399
Args,
33893400
Return,
33903401
F: (...Array<Args>) => Return,
3391-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3402+
>(callback: F): F {
33923403
currentHookNameInDev = 'useEvent';
33933404
updateHookTypesDev();
33943405
return updateEvent(callback);
@@ -3572,7 +3583,7 @@ if (__DEV__) {
35723583
Args,
35733584
Return,
35743585
F: (...Array<Args>) => Return,
3575-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3586+
>(callback: F): F {
35763587
currentHookNameInDev = 'useEvent';
35773588
warnInvalidHookAccess();
35783589
mountHookTypesDev();
@@ -3757,7 +3768,7 @@ if (__DEV__) {
37573768
Args,
37583769
Return,
37593770
F: (...Array<Args>) => Return,
3760-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3771+
>(callback: F): F {
37613772
currentHookNameInDev = 'useEvent';
37623773
warnInvalidHookAccess();
37633774
updateHookTypesDev();
@@ -3943,7 +3954,7 @@ if (__DEV__) {
39433954
Args,
39443955
Return,
39453956
F: (...Array<Args>) => Return,
3946-
>(callback: F): EventFunctionWrapper<Args, Return, F> {
3957+
>(callback: F): F {
39473958
currentHookNameInDev = 'useEvent';
39483959
warnInvalidHookAccess();
39493960
updateHookTypesDev();

0 commit comments

Comments
 (0)