@samitouri / QOS-React-2 / commits / 3517bd9f77

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