From 32a6646d63888b30b165abfe30ced20c51cf861a Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Wed, 12 Oct 2022 17:31:23 -0400 Subject: [PATCH 1/5] [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. --- .../src/ReactFiberHooks.new.js | 13 +++--- .../src/ReactFiberHooks.old.js | 13 +++--- .../src/__tests__/useEvent-test.js | 43 ++++++++++++++++++- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 8183ddd1557c0..acb115f93f0b1 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -1928,10 +1928,9 @@ function useEventImpl) => Return>( } } -function mountEvent) => Return>( +function wrapEventFunction) => Return>( callback: F, ): EventFunctionWrapper { - const hook = mountWorkInProgressHook(); const eventFn: EventFunctionWrapper = function eventFn() { if (isInvalidExecutionContextForEventFunction()) { throw new Error( @@ -1942,17 +1941,21 @@ function mountEvent) => Return>( return eventFn._impl.apply(undefined, arguments); }; eventFn._impl = callback; + return eventFn; +} +function mountEvent) => Return>( + callback: F, +): EventFunctionWrapper { + const eventFn = wrapEventFunction(callback); useEventImpl(eventFn, callback); - hook.memoizedState = eventFn; return eventFn; } function updateEvent) => Return>( callback: F, ): EventFunctionWrapper { - const hook = updateWorkInProgressHook(); - const eventFn = hook.memoizedState; + const eventFn = wrapEventFunction(callback); useEventImpl(eventFn, callback); return eventFn; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 97aa7dcd4a41a..78cb130fc0ced 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -1928,10 +1928,9 @@ function useEventImpl) => Return>( } } -function mountEvent) => Return>( +function wrapEventFunction) => Return>( callback: F, ): EventFunctionWrapper { - const hook = mountWorkInProgressHook(); const eventFn: EventFunctionWrapper = function eventFn() { if (isInvalidExecutionContextForEventFunction()) { throw new Error( @@ -1942,17 +1941,21 @@ function mountEvent) => Return>( return eventFn._impl.apply(undefined, arguments); }; eventFn._impl = callback; + return eventFn; +} +function mountEvent) => Return>( + callback: F, +): EventFunctionWrapper { + const eventFn = wrapEventFunction(callback); useEventImpl(eventFn, callback); - hook.memoizedState = eventFn; return eventFn; } function updateEvent) => Return>( callback: F, ): EventFunctionWrapper { - const hook = updateWorkInProgressHook(); - const eventFn = hook.memoizedState; + const eventFn = wrapEventFunction(callback); useEventImpl(eventFn, callback); return eventFn; } diff --git a/packages/react-reconciler/src/__tests__/useEvent-test.js b/packages/react-reconciler/src/__tests__/useEvent-test.js index e3b0d872f857e..f69e996285038 100644 --- a/packages/react-reconciler/src/__tests__/useEvent-test.js +++ b/packages/react-reconciler/src/__tests__/useEvent-test.js @@ -557,6 +557,45 @@ describe('useEvent', () => { expect(Scheduler).toHaveYielded(['Effect value: 2', 'Event value: 2']); }); + // @gate enableUseEventHook + it("doesn't provide a stable identity", () => { + function Counter({shouldRender, value}) { + const onClick = useEvent(() => { + Scheduler.unstable_yieldValue( + 'onClick, shouldRender=' + shouldRender + ', value=' + value, + ); + }); + + // onClick doesn't have a stable function identity so this effect will fire on every render. + // In a real app useEvent functions should *not* be passed as a dependency, this is for + // testing purposes only. + useEffect(() => { + onClick(); + }, [onClick]); + + useEffect(() => { + onClick(); + }, [shouldRender]); + + return <>; + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'onClick, shouldRender=true, value=0', + 'onClick, shouldRender=true, value=0', + ]); + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['onClick, shouldRender=true, value=1']); + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'onClick, shouldRender=false, value=2', + 'onClick, shouldRender=false, value=2', + ]); + }); + // @gate enableUseEventHook it('integration: implements docs chat room example', () => { function createConnection() { @@ -597,7 +636,7 @@ describe('useEvent', () => { }); connection.connect(); return () => connection.disconnect(); - }, [roomId, onConnected]); + }, [roomId]); return ; } @@ -676,7 +715,7 @@ describe('useEvent', () => { const onVisit = useEvent(visitedUrl => { Scheduler.unstable_yieldValue( - 'url: ' + url + ', numberOfItems: ' + numberOfItems, + 'url: ' + visitedUrl + ', numberOfItems: ' + numberOfItems, ); }); From b69bb5dd763ce68ede7ec4b6fec13ccb542626b5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 18 Oct 2022 12:20:26 -0400 Subject: [PATCH 2/5] 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. --- .../src/__tests__/useEvent-test.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/react-reconciler/src/__tests__/useEvent-test.js b/packages/react-reconciler/src/__tests__/useEvent-test.js index f69e996285038..fecf99679d263 100644 --- a/packages/react-reconciler/src/__tests__/useEvent-test.js +++ b/packages/react-reconciler/src/__tests__/useEvent-test.js @@ -596,6 +596,56 @@ describe('useEvent', () => { ]); }); + // @gate enableUseEventHook + it('event handlers always see the latest committed value', async () => { + let committedEventHandler = null; + + function App({value}) { + const event = useEvent(() => { + return 'Value seen by useEvent: ' + value; + }); + + // Set up an effect that registers the event handler with an external + // event system (e.g. addEventListener). + useEffect( + () => { + // Log when the effect fires. In the test below, we'll assert that this + // only happens during initial render, not during updates. + Scheduler.unstable_yieldValue('Commit new event handler'); + committedEventHandler = event; + return () => { + committedEventHandler = null; + }; + }, + // Note that we've intentionally omitted the event from the dependency + // array. But it will still be able to see the latest `value`. This is the + // key feature of useEvent that makes it different from a regular closure. + [], + ); + return 'Latest rendered value ' + value; + } + + // Initial render + const root = ReactNoop.createRoot(); + await act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Commit new event handler']); + expect(root).toMatchRenderedOutput('Latest rendered value 1'); + expect(committedEventHandler()).toBe('Value seen by useEvent: 1'); + + // Update + await act(async () => { + root.render(); + }); + // No new event handler should be committed, because it was omitted from + // the dependency array. + expect(Scheduler).toHaveYielded([]); + // But the event handler should still be able to see the latest value. + expect(root).toMatchRenderedOutput('Latest rendered value 2'); + expect(committedEventHandler()).toBe('Value seen by useEvent: 2'); + }); + // @gate enableUseEventHook it('integration: implements docs chat room example', () => { function createConnection() { From 96ed5e0f1fe0d10fcdaa2eca779ee56473be8bf3 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Tue, 18 Oct 2022 12:20:26 -0400 Subject: [PATCH 3/5] Fix failing test --- packages/react-reconciler/src/ReactFiberHooks.new.js | 8 ++++++-- packages/react-reconciler/src/ReactFiberHooks.old.js | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index acb115f93f0b1..bfbf628f38de8 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -1947,7 +1947,9 @@ function wrapEventFunction) => Return>( function mountEvent) => Return>( callback: F, ): EventFunctionWrapper { + const hook = mountWorkInProgressHook(); const eventFn = wrapEventFunction(callback); + hook.memoizedState = eventFn; useEventImpl(eventFn, callback); return eventFn; } @@ -1955,9 +1957,11 @@ function mountEvent) => Return>( function updateEvent) => Return>( callback: F, ): EventFunctionWrapper { - const eventFn = wrapEventFunction(callback); + const hook = updateWorkInProgressHook(); + const eventFn = hook.memoizedState; useEventImpl(eventFn, callback); - return eventFn; + // Always return a new function + return wrapEventFunction(callback); } function mountInsertionEffect( diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 78cb130fc0ced..9e1707e4e31cb 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -1947,7 +1947,9 @@ function wrapEventFunction) => Return>( function mountEvent) => Return>( callback: F, ): EventFunctionWrapper { + const hook = mountWorkInProgressHook(); const eventFn = wrapEventFunction(callback); + hook.memoizedState = eventFn; useEventImpl(eventFn, callback); return eventFn; } @@ -1955,9 +1957,11 @@ function mountEvent) => Return>( function updateEvent) => Return>( callback: F, ): EventFunctionWrapper { - const eventFn = wrapEventFunction(callback); + const hook = updateWorkInProgressHook(); + const eventFn = hook.memoizedState; useEventImpl(eventFn, callback); - return eventFn; + // Always return a new function + return wrapEventFunction(callback); } function mountInsertionEffect( From ef85a49d2a9a5564eee66adab487ddb54967c3ae Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Wed, 19 Oct 2022 13:46:43 -0400 Subject: [PATCH 4/5] Don't queue a commit effect on mount --- packages/react-reconciler/src/ReactFiberHooks.new.js | 1 - packages/react-reconciler/src/ReactFiberHooks.old.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index bfbf628f38de8..d36908cf555a3 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -1950,7 +1950,6 @@ function mountEvent) => Return>( const hook = mountWorkInProgressHook(); const eventFn = wrapEventFunction(callback); hook.memoizedState = eventFn; - useEventImpl(eventFn, callback); return eventFn; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 9e1707e4e31cb..e0840890ded04 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -1950,7 +1950,6 @@ function mountEvent) => Return>( const hook = mountWorkInProgressHook(); const eventFn = wrapEventFunction(callback); hook.memoizedState = eventFn; - useEventImpl(eventFn, callback); return eventFn; } From c8ab9979e10427cca5a4c894826246b405f49b93 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Wed, 19 Oct 2022 14:47:41 -0400 Subject: [PATCH 5/5] Inline event function wrapping - Inlines wrapping of the callback - Use a mutable ref-style object instead of a callable object - Fix types --- .../src/ReactFiberCommitWork.new.js | 16 +--- .../src/ReactFiberCommitWork.old.js | 16 +--- .../src/ReactFiberHooks.new.js | 75 ++++++++++--------- .../src/ReactFiberHooks.old.js | 75 ++++++++++--------- .../src/ReactInternalTypes.js | 15 +--- packages/react-server/src/ReactFizzHooks.js | 8 +- 6 files changed, 92 insertions(+), 113 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index ea8e0cf26354b..e16aa04507961 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -15,11 +15,7 @@ import type { ChildSet, UpdatePayload, } from './ReactFiberHostConfig'; -import type { - Fiber, - FiberRoot, - EventFunctionWrapper, -} from './ReactInternalTypes'; +import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.new'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new'; @@ -689,13 +685,9 @@ function commitUseEventMount(finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const eventPayloads = updateQueue !== null ? updateQueue.events : null; if (eventPayloads !== null) { - // FunctionComponentUpdateQueue.events is a flat array of - // [EventFunctionWrapper, EventFunction, ...], so increment by 2 each iteration to find the next - // pair. - for (let ii = 0; ii < eventPayloads.length; ii += 2) { - const eventFn: EventFunctionWrapper = eventPayloads[ii]; - const nextImpl = eventPayloads[ii + 1]; - eventFn._impl = nextImpl; + for (let ii = 0; ii < eventPayloads.length; ii++) { + const {ref, nextImpl} = eventPayloads[ii]; + ref.impl = nextImpl; } } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index b33a051ee09d0..4aa073d0db31a 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -15,11 +15,7 @@ import type { ChildSet, UpdatePayload, } from './ReactFiberHostConfig'; -import type { - Fiber, - FiberRoot, - EventFunctionWrapper, -} from './ReactInternalTypes'; +import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.old'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; @@ -689,13 +685,9 @@ function commitUseEventMount(finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const eventPayloads = updateQueue !== null ? updateQueue.events : null; if (eventPayloads !== null) { - // FunctionComponentUpdateQueue.events is a flat array of - // [EventFunctionWrapper, EventFunction, ...], so increment by 2 each iteration to find the next - // pair. - for (let ii = 0; ii < eventPayloads.length; ii += 2) { - const eventFn: EventFunctionWrapper = eventPayloads[ii]; - const nextImpl = eventPayloads[ii + 1]; - eventFn._impl = nextImpl; + for (let ii = 0; ii < eventPayloads.length; ii++) { + const {ref, nextImpl} = eventPayloads[ii]; + ref.impl = nextImpl; } } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index d36908cf555a3..2e399e7897794 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -22,7 +22,6 @@ import type { Dispatcher, HookType, MemoCache, - EventFunctionWrapper, } from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; import type {HookFlags} from './ReactHookEffectTags'; @@ -189,9 +188,17 @@ type StoreConsistencyCheck = { getSnapshot: () => T, }; +type EventFunctionPayload) => Return> = { + ref: { + eventFn: F, + impl: F, + }, + nextImpl: F, +}; + export type FunctionComponentUpdateQueue = { lastEffect: Effect | null, - events: Array<() => mixed> | null, + events: Array> | null, stores: Array> | null, // NOTE: optional, only set when enableUseMemoCacheHook is enabled memoCache?: MemoCache | null, @@ -1909,58 +1916,56 @@ function updateEffect( } function useEventImpl) => Return>( - event: EventFunctionWrapper, - nextImpl: F, + payload: EventFunctionPayload, ) { currentlyRenderingFiber.flags |= UpdateEffect; let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); - componentUpdateQueue.events = [event, nextImpl]; + componentUpdateQueue.events = [payload]; } else { const events = componentUpdateQueue.events; if (events === null) { - componentUpdateQueue.events = [event, nextImpl]; + componentUpdateQueue.events = [payload]; } else { - events.push(event, nextImpl); + events.push(payload); } } } -function wrapEventFunction) => Return>( +function mountEvent) => Return>( callback: F, -): EventFunctionWrapper { - const eventFn: EventFunctionWrapper = function eventFn() { +): F { + const hook = mountWorkInProgressHook(); + const ref = {impl: callback}; + hook.memoizedState = ref; + // $FlowIgnore[incompatible-return] + return function eventFn() { if (isInvalidExecutionContextForEventFunction()) { throw new Error( "A function wrapped in useEvent can't be called during rendering.", ); } - // $FlowFixMe[prop-missing] found when upgrading Flow - return eventFn._impl.apply(undefined, arguments); + return ref.impl.apply(undefined, arguments); }; - eventFn._impl = callback; - return eventFn; -} - -function mountEvent) => Return>( - callback: F, -): EventFunctionWrapper { - const hook = mountWorkInProgressHook(); - const eventFn = wrapEventFunction(callback); - hook.memoizedState = eventFn; - return eventFn; } function updateEvent) => Return>( callback: F, -): EventFunctionWrapper { +): F { const hook = updateWorkInProgressHook(); - const eventFn = hook.memoizedState; - useEventImpl(eventFn, callback); - // Always return a new function - return wrapEventFunction(callback); + const ref = hook.memoizedState; + useEventImpl({ref, nextImpl: callback}); + // $FlowIgnore[incompatible-return] + return function eventFn() { + if (isInvalidExecutionContextForEventFunction()) { + throw new Error( + "A function wrapped in useEvent can't be called during rendering.", + ); + } + return ref.impl.apply(undefined, arguments); + }; } function mountInsertionEffect( @@ -2922,7 +2927,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; mountHookTypesDev(); return mountEvent(callback); @@ -3079,7 +3084,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; updateHookTypesDev(); return mountEvent(callback); @@ -3236,7 +3241,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; updateHookTypesDev(); return updateEvent(callback); @@ -3394,7 +3399,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; updateHookTypesDev(); return updateEvent(callback); @@ -3578,7 +3583,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; warnInvalidHookAccess(); mountHookTypesDev(); @@ -3763,7 +3768,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; warnInvalidHookAccess(); updateHookTypesDev(); @@ -3949,7 +3954,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; warnInvalidHookAccess(); updateHookTypesDev(); diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index e0840890ded04..7c4a705517346 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -22,7 +22,6 @@ import type { Dispatcher, HookType, MemoCache, - EventFunctionWrapper, } from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.old'; import type {HookFlags} from './ReactHookEffectTags'; @@ -189,9 +188,17 @@ type StoreConsistencyCheck = { getSnapshot: () => T, }; +type EventFunctionPayload) => Return> = { + ref: { + eventFn: F, + impl: F, + }, + nextImpl: F, +}; + export type FunctionComponentUpdateQueue = { lastEffect: Effect | null, - events: Array<() => mixed> | null, + events: Array> | null, stores: Array> | null, // NOTE: optional, only set when enableUseMemoCacheHook is enabled memoCache?: MemoCache | null, @@ -1909,58 +1916,56 @@ function updateEffect( } function useEventImpl) => Return>( - event: EventFunctionWrapper, - nextImpl: F, + payload: EventFunctionPayload, ) { currentlyRenderingFiber.flags |= UpdateEffect; let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); - componentUpdateQueue.events = [event, nextImpl]; + componentUpdateQueue.events = [payload]; } else { const events = componentUpdateQueue.events; if (events === null) { - componentUpdateQueue.events = [event, nextImpl]; + componentUpdateQueue.events = [payload]; } else { - events.push(event, nextImpl); + events.push(payload); } } } -function wrapEventFunction) => Return>( +function mountEvent) => Return>( callback: F, -): EventFunctionWrapper { - const eventFn: EventFunctionWrapper = function eventFn() { +): F { + const hook = mountWorkInProgressHook(); + const ref = {impl: callback}; + hook.memoizedState = ref; + // $FlowIgnore[incompatible-return] + return function eventFn() { if (isInvalidExecutionContextForEventFunction()) { throw new Error( "A function wrapped in useEvent can't be called during rendering.", ); } - // $FlowFixMe[prop-missing] found when upgrading Flow - return eventFn._impl.apply(undefined, arguments); + return ref.impl.apply(undefined, arguments); }; - eventFn._impl = callback; - return eventFn; -} - -function mountEvent) => Return>( - callback: F, -): EventFunctionWrapper { - const hook = mountWorkInProgressHook(); - const eventFn = wrapEventFunction(callback); - hook.memoizedState = eventFn; - return eventFn; } function updateEvent) => Return>( callback: F, -): EventFunctionWrapper { +): F { const hook = updateWorkInProgressHook(); - const eventFn = hook.memoizedState; - useEventImpl(eventFn, callback); - // Always return a new function - return wrapEventFunction(callback); + const ref = hook.memoizedState; + useEventImpl({ref, nextImpl: callback}); + // $FlowIgnore[incompatible-return] + return function eventFn() { + if (isInvalidExecutionContextForEventFunction()) { + throw new Error( + "A function wrapped in useEvent can't be called during rendering.", + ); + } + return ref.impl.apply(undefined, arguments); + }; } function mountInsertionEffect( @@ -2922,7 +2927,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; mountHookTypesDev(); return mountEvent(callback); @@ -3079,7 +3084,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; updateHookTypesDev(); return mountEvent(callback); @@ -3236,7 +3241,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; updateHookTypesDev(); return updateEvent(callback); @@ -3394,7 +3399,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; updateHookTypesDev(); return updateEvent(callback); @@ -3578,7 +3583,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; warnInvalidHookAccess(); mountHookTypesDev(); @@ -3763,7 +3768,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; warnInvalidHookAccess(); updateHookTypesDev(); @@ -3949,7 +3954,7 @@ if (__DEV__) { Args, Return, F: (...Array) => Return, - >(callback: F): EventFunctionWrapper { + >(callback: F): F { currentHookNameInDev = 'useEvent'; warnInvalidHookAccess(); updateHookTypesDev(); diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 10c4c9853a300..a5f28fd5d2450 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -290,17 +290,6 @@ type SuspenseCallbackOnlyFiberRootProperties = { hydrationCallbacks: null | SuspenseHydrationCallbacks, }; -// A wrapper callable object around a useEvent callback that throws if the callback is called during -// rendering. The _impl property points to the actual implementation. -export type EventFunctionWrapper< - Args, - Return, - F: (...Array) => Return, -> = { - (): F, - _impl: F, -}; - export type TransitionTracingCallbacks = { onTransitionStart?: (transitionName: string, startTime: number) => void, onTransitionProgress?: ( @@ -390,9 +379,7 @@ export type Dispatcher = { create: () => (() => void) | void, deps: Array | void | null, ): void, - useEvent?: ) => Return>( - callback: F, - ) => EventFunctionWrapper, + useEvent?: ) => Return>(callback: F) => F, useInsertionEffect( create: () => (() => void) | void, deps: Array | void | null, diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index c8ceeddc400e4..a9295b3143008 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -7,10 +7,7 @@ * @flow */ -import type { - Dispatcher, - EventFunctionWrapper, -} from 'react-reconciler/src/ReactInternalTypes'; +import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; import type { MutableSource, @@ -522,7 +519,8 @@ function throwOnUseEventCall() { export function useEvent) => Return>( callback: F, -): EventFunctionWrapper { +): F { + // $FlowIgnore[incompatible-return] return throwOnUseEventCall; }