From 57992c40a384b8ac35cb71c02e1c1aab184c4988 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 1 Dec 2025 11:35:45 +0100 Subject: [PATCH 1/8] Reduce number of closures --- .../js/useGestureStateChangeEvent.ts | 28 ---- .../callbacks/js/useGestureTouchEvent.ts | 25 ---- .../callbacks/js/useGestureUpdateEvent.ts | 35 ----- .../src/v3/hooks/callbacks/stateMachine.ts | 121 ++++++++++++++++++ .../hooks/callbacks/useGestureEventHandler.ts | 39 ++++++ .../callbacks/useReanimatedEventHandler.ts | 35 ++--- .../src/v3/hooks/useGesture.ts | 46 +++---- .../src/v3/hooks/useGestureCallbacks.ts | 43 ++----- .../src/v3/hooks/utils/eventHandlersUtils.ts | 83 +++++------- .../src/v3/hooks/utils/eventUtils.ts | 5 +- 10 files changed, 228 insertions(+), 232 deletions(-) delete mode 100644 packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts delete mode 100644 packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts delete mode 100644 packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts create mode 100644 packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts create mode 100644 packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts deleted file mode 100644 index 22120f8af4..0000000000 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useMemo } from 'react'; -import { BaseGestureConfig } from '../../../types'; -import { prepareStateChangeHandlers } from '../../utils'; -import { getStateChangeHandler } from '../stateChangeHandler'; -import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper'; - -export function useGestureStateChangeEvent( - handlerTag: number, - config: BaseGestureConfig, - context: ReanimatedContext -) { - return useMemo(() => { - const handlers = prepareStateChangeHandlers({ - onBegin: config.onBegin, - onActivate: config.onActivate, - onDeactivate: config.onDeactivate, - onFinalize: config.onFinalize, - }); - return getStateChangeHandler(handlerTag, handlers, context); - }, [ - handlerTag, - config.onBegin, - config.onActivate, - config.onDeactivate, - config.onFinalize, - context, - ]); -} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts deleted file mode 100644 index aee137df1d..0000000000 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useMemo } from 'react'; -import { BaseGestureConfig } from '../../../types'; -import { prepareTouchHandlers } from '../../utils'; -import { getTouchEventHandler } from '../touchEventHandler'; - -export function useGestureTouchEvent( - handlerTag: number, - config: BaseGestureConfig -) { - return useMemo(() => { - const handlers = prepareTouchHandlers({ - onTouchesDown: config.onTouchesDown, - onTouchesMove: config.onTouchesMove, - onTouchesUp: config.onTouchesUp, - onTouchesCancel: config.onTouchesCancel, - }); - return getTouchEventHandler(handlerTag, handlers); - }, [ - handlerTag, - config.onTouchesDown, - config.onTouchesMove, - config.onTouchesUp, - config.onTouchesCancel, - ]); -} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts deleted file mode 100644 index 587d28b683..0000000000 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { prepareUpdateHandlers } from '../../utils'; -import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper'; -import { getUpdateHandler } from '../updateHandler'; -import { BaseGestureConfig } from '../../../types'; -import { useMemo } from 'react'; - -export function useGestureUpdateEvent( - handlerTag: number, - config: BaseGestureConfig, - jsContext: ReanimatedContext -) { - return useMemo(() => { - const { handlers, changeEventCalculator } = prepareUpdateHandlers( - { - onUpdate: config.onUpdate, - }, - config.changeEventCalculator - ); - - return config.dispatchesAnimatedEvents - ? undefined - : getUpdateHandler( - handlerTag, - handlers, - jsContext, - changeEventCalculator - ); - }, [ - handlerTag, - config.onUpdate, - config.dispatchesAnimatedEvents, - config.changeEventCalculator, - jsContext, - ]); -} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts new file mode 100644 index 0000000000..fd67e84476 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts @@ -0,0 +1,121 @@ +import { + flattenEvent, + isEventForHandlerWithTag, + maybeExtractNativeEvent, + runCallback, + touchEventTypeToCallbackType, +} from '../utils'; +import { tagMessage } from '../../../utils'; +import { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper'; +import { + ChangeCalculatorType, + GestureCallbacks, + GestureHandlerEventWithHandlerData, + GestureStateChangeEventWithHandlerData, + GestureUpdateEventWithHandlerData, +} from '../../types'; +import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; +import { State } from '../../../State'; +import { TouchEventType } from '../../../TouchEventType'; +import { GestureTouchEvent } from '../../../handlers/gestureHandlerCommon'; + +function handleStateChange( + eventWithData: GestureStateChangeEventWithHandlerData, + callbacks: GestureCallbacks, + context: ReanimatedContext +) { + 'worklet'; + const event = flattenEvent(eventWithData); + + if (event.oldState === State.UNDETERMINED && event.state === State.BEGAN) { + runCallback(CALLBACK_TYPE.BEGAN, callbacks, event); + } else if ( + (event.oldState === State.BEGAN || event.oldState === State.UNDETERMINED) && + event.state === State.ACTIVE + ) { + runCallback(CALLBACK_TYPE.START, callbacks, event); + } else if (event.oldState !== event.state && event.state === State.END) { + if (event.oldState === State.ACTIVE) { + runCallback(CALLBACK_TYPE.END, callbacks, event, true); + } + runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, true); + + if (context) { + context.lastUpdateEvent = undefined; + } + } else if ( + (event.state === State.FAILED || event.state === State.CANCELLED) && + event.state !== event.oldState + ) { + if (event.oldState === State.ACTIVE) { + runCallback(CALLBACK_TYPE.END, callbacks, event, false); + } + runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, false); + + if (context) { + context.lastUpdateEvent = undefined; + } + } +} + +export function handleUpdate( + eventWithData: GestureUpdateEventWithHandlerData, + handlers: GestureCallbacks, + changeEventCalculator: ChangeCalculatorType | undefined, + context: ReanimatedContext +) { + 'worklet'; + const eventWithChanges = changeEventCalculator + ? changeEventCalculator( + eventWithData, + context ? context.lastUpdateEvent : undefined + ) + : eventWithData; + + const event = flattenEvent(eventWithChanges); + + // This should never happen, but since we don't want to call hooks conditionally, we have to mark + // context as possibly undefined to make TypeScript happy. + if (!context) { + throw new Error(tagMessage('Event handler context is not defined')); + } + + runCallback(CALLBACK_TYPE.UPDATE, handlers, event); + + context.lastUpdateEvent = eventWithData; +} + +export function handleTouchEvent( + event: GestureTouchEvent, + handlers: GestureCallbacks +) { + 'worklet'; + + if (event.eventType !== TouchEventType.UNDETERMINED) { + runCallback(touchEventTypeToCallbackType(event.eventType), handlers, event); + } +} + +export function stateMachine( + handlerTag: number, + sourceEvent: GestureHandlerEventWithHandlerData, + handlers: GestureCallbacks, + changeEventCalculator: ChangeCalculatorType | undefined, + jsContext: ReanimatedContext, + dispatchesAnimatedEvents: boolean +) { + 'worklet'; + const eventWithData = maybeExtractNativeEvent(sourceEvent); + + if (!isEventForHandlerWithTag(handlerTag, eventWithData)) { + return; + } + + if ('oldState' in eventWithData && eventWithData.oldState !== undefined) { + handleStateChange(eventWithData, handlers, jsContext); + } else if ('allTouches' in eventWithData) { + handleTouchEvent(eventWithData, handlers); + } else if (!dispatchesAnimatedEvents) { + handleUpdate(eventWithData, handlers, changeEventCalculator, jsContext); + } +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts new file mode 100644 index 0000000000..827a8c5a63 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts @@ -0,0 +1,39 @@ +import { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper'; +import { + BaseGestureConfig, + GestureCallbacks, + GestureHandlerEventWithHandlerData, +} from '../../types'; +import { useMemo } from 'react'; +import { stateMachine } from './stateMachine'; + +export function useGestureEventHandler( + handlerTag: number, + handlers: GestureCallbacks, + config: BaseGestureConfig +) { + const jsContext: ReanimatedContext = useMemo(() => { + return { + lastUpdateEvent: undefined, + }; + }, []); + + return useMemo(() => { + return (event: GestureHandlerEventWithHandlerData) => { + stateMachine( + handlerTag, + event, + handlers, + config.changeEventCalculator, + jsContext, + !!config.dispatchesAnimatedEvents + ); + }; + }, [ + handlerTag, + handlers, + config.changeEventCalculator, + config.dispatchesAnimatedEvents, + jsContext, + ]); +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts index edfbbfdb3d..ee236934ae 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts @@ -7,9 +7,7 @@ import { GestureCallbacks, UnpackedGestureHandlerEventWithHandlerData, } from '../../types'; -import { getStateChangeHandler } from './stateChangeHandler'; -import { getTouchEventHandler } from './touchEventHandler'; -import { getUpdateHandler } from './updateHandler'; +import { stateMachine } from './stateMachine'; export function useReanimatedEventHandler( handlerTag: number, @@ -28,32 +26,19 @@ export function useReanimatedEventHandler( }; } - const stateChangeCallback = getStateChangeHandler( - handlerTag, - handlers, - reanimatedHandler?.context - ); - - const updateCallback = getUpdateHandler( - handlerTag, - handlers, - reanimatedHandler?.context, - changeEventCalculator - ); - - const touchCallback = getTouchEventHandler(handlerTag, handlers); - const callback = ( event: UnpackedGestureHandlerEventWithHandlerData ) => { 'worklet'; - if ('oldState' in event && event.oldState !== undefined) { - stateChangeCallback(event); - } else if ('allTouches' in event) { - touchCallback(event); - } else { - updateCallback(event); - } + stateMachine( + handlerTag, + event, + handlers, + changeEventCalculator, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain + reanimatedHandler?.context!, + false + ); }; const reanimatedEvent = Reanimated?.useEvent( diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts index 4c6a920299..4e8af7ee99 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -33,40 +33,26 @@ export function useGesture( // TODO: Call only necessary hooks depending on which callbacks are defined (?) const { - onGestureHandlerStateChange, onGestureHandlerEvent, - onGestureHandlerTouchEvent, onReanimatedEvent, onGestureHandlerAnimatedEvent, } = useGestureCallbacks(tag, config); - // This should never happen, but since we don't want to call hooks conditionally, - // we have to mark these as possibly undefined to make TypeScript happy. - if ( - !onGestureHandlerStateChange || - // If onUpdate is an AnimatedEvent, `onGestureHandlerEvent` will be undefined and vice versa. - (!onGestureHandlerEvent && !onGestureHandlerAnimatedEvent) || - !onGestureHandlerTouchEvent - ) { - throw new Error(tagMessage('Failed to create event handlers.')); - } - if (config.shouldUseReanimatedDetector && !onReanimatedEvent) { throw new Error(tagMessage('Failed to create reanimated event handlers.')); } - const gestureRelations = useMemo( - () => - prepareRelations( - { - simultaneousWith: config.simultaneousWith, - requireToFail: config.requireToFail, - block: config.block, - }, - tag - ), - [tag, config.simultaneousWith, config.requireToFail, config.block] - ); + const gestureRelations = useMemo(() => { + const rel = prepareRelations( + { + simultaneousWith: config.simultaneousWith, + requireToFail: config.requireToFail, + block: config.block, + }, + tag + ); + return rel; + }, [tag, config.simultaneousWith, config.requireToFail, config.block]); const currentGestureRef = useRef({ type: '', tag: -1 }); if ( @@ -95,14 +81,14 @@ export function useGesture( }, [tag, config, type]); return useMemo( - () => ({ + (): SingleGesture => ({ tag, type, config, detectorCallbacks: { - onGestureHandlerStateChange, - onGestureHandlerEvent, - onGestureHandlerTouchEvent, + onGestureHandlerStateChange: onGestureHandlerEvent, + onGestureHandlerEvent: onGestureHandlerEvent, + onGestureHandlerTouchEvent: onGestureHandlerEvent, onGestureHandlerAnimatedEvent, // On web, we're triggering Reanimated callbacks ourselves, based on the type. // To handle this properly, we need to provide all three callbacks, so we set @@ -128,9 +114,7 @@ export function useGesture( tag, type, config, - onGestureHandlerStateChange, onGestureHandlerEvent, - onGestureHandlerTouchEvent, onReanimatedEvent, onGestureHandlerAnimatedEvent, gestureRelations, diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts b/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts index 40f45bdb6b..4704b83999 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts @@ -1,6 +1,4 @@ -import { useGestureStateChangeEvent } from './callbacks/js/useGestureStateChangeEvent'; -import { useGestureUpdateEvent } from './callbacks/js/useGestureUpdateEvent'; -import { useGestureTouchEvent } from './callbacks/js/useGestureTouchEvent'; +import { useGestureEventHandler } from './callbacks/useGestureEventHandler'; import { AnimatedEvent, BaseGestureConfig, @@ -9,17 +7,11 @@ import { import { checkMappingForChangeProperties, isNativeAnimatedEvent, - prepareStateChangeHandlers, - prepareTouchHandlers, - prepareUpdateHandlers, + useMemoizedGestureCallbacks, } from './utils'; import { useReanimatedEventHandler } from './callbacks/useReanimatedEventHandler'; import { tagMessage } from '../../utils'; -import { - Reanimated, - ReanimatedContext, -} from '../../handlers/gestures/reanimatedWrapper'; -import { useMemo } from 'react'; +import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; function guardJSAnimatedEvent(handler: (...args: unknown[]) => void) { return (...args: unknown[]) => { @@ -48,38 +40,23 @@ export function useGestureCallbacks( handlerTag: number, config: BaseGestureConfig ) { - const jsContext: ReanimatedContext = useMemo(() => { - return { - lastUpdateEvent: undefined, - }; - }, []); + const callbacks = useMemoizedGestureCallbacks(config); - const onGestureHandlerStateChange = useGestureStateChangeEvent( - handlerTag, - config, - jsContext - ); - const onGestureHandlerEvent = useGestureUpdateEvent( + const onGestureHandlerEvent = useGestureEventHandler( handlerTag, - config, - jsContext + callbacks, + config ); - const onGestureHandlerTouchEvent = useGestureTouchEvent(handlerTag, config); let onReanimatedEvent; if (!config.disableReanimated) { - const handlers = { - ...prepareStateChangeHandlers(config), - ...prepareUpdateHandlers(config).handlers, - ...prepareTouchHandlers(config), - }; // eslint-disable-next-line react-hooks/rules-of-hooks - const reanimatedHandler = Reanimated?.useHandler(handlers); + const reanimatedHandler = Reanimated?.useHandler(callbacks); // eslint-disable-next-line react-hooks/rules-of-hooks onReanimatedEvent = useReanimatedEventHandler( handlerTag, - handlers, + callbacks, reanimatedHandler, config.changeEventCalculator ); @@ -104,9 +81,7 @@ export function useGestureCallbacks( } return { - onGestureHandlerStateChange, onGestureHandlerEvent, - onGestureHandlerTouchEvent, onReanimatedEvent, onGestureHandlerAnimatedEvent, }; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts index ad3d006dbb..9ce9119f30 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts @@ -1,60 +1,37 @@ +import { useMemo } from 'react'; import { TouchEventType } from '../../../TouchEventType'; import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; -import { - ChangeCalculatorType, - GestureCallbacks, - UnpackedGestureHandlerEvent, -} from '../../types'; +import { GestureCallbacks, UnpackedGestureHandlerEvent } from '../../types'; -export function prepareStateChangeHandlers( - callbacks: GestureCallbacks +export function useMemoizedGestureCallbacks( + config: GestureCallbacks ): GestureCallbacks { - 'worklet'; - const { onBegin, onActivate, onDeactivate, onFinalize } = callbacks; - - const handlers: GestureCallbacks = { - ...(onBegin ? { onBegin } : {}), - ...(onActivate ? { onActivate } : {}), - ...(onDeactivate ? { onDeactivate } : {}), - ...(onFinalize ? { onFinalize } : {}), - }; - - return handlers; -} - -type UpdateHandlersReturnType = { - handlers: GestureCallbacks; - changeEventCalculator?: ChangeCalculatorType; -}; - -export function prepareUpdateHandlers( - callbacks: GestureCallbacks, - changeEventCalculator?: ChangeCalculatorType -): UpdateHandlersReturnType { - 'worklet'; - const { onUpdate } = callbacks; - - const handlers: GestureCallbacks = { - ...(onUpdate ? { onUpdate } : {}), - }; - - return { handlers, changeEventCalculator }; -} - -export function prepareTouchHandlers( - callbacks: GestureCallbacks -): GestureCallbacks { - const { onTouchesDown, onTouchesMove, onTouchesUp, onTouchesCancel } = - callbacks; - - const handlers: GestureCallbacks = { - ...(onTouchesDown ? { onTouchesDown } : {}), - ...(onTouchesMove ? { onTouchesMove } : {}), - ...(onTouchesUp ? { onTouchesUp } : {}), - ...(onTouchesCancel ? { onTouchesCancel } : {}), - }; - - return handlers; + return useMemo( + () => ({ + ...(config.onBegin ? { onBegin: config.onBegin } : {}), + ...(config.onActivate ? { onActivate: config.onActivate } : {}), + ...(config.onDeactivate ? { onDeactivate: config.onDeactivate } : {}), + ...(config.onFinalize ? { onFinalize: config.onFinalize } : {}), + ...(config.onUpdate ? { onUpdate: config.onUpdate } : {}), + ...(config.onTouchesDown ? { onTouchesDown: config.onTouchesDown } : {}), + ...(config.onTouchesMove ? { onTouchesMove: config.onTouchesMove } : {}), + ...(config.onTouchesUp ? { onTouchesUp: config.onTouchesUp } : {}), + ...(config.onTouchesCancel + ? { onTouchesCancel: config.onTouchesCancel } + : {}), + }), + [ + config.onActivate, + config.onBegin, + config.onDeactivate, + config.onFinalize, + config.onTouchesCancel, + config.onTouchesDown, + config.onTouchesMove, + config.onTouchesUp, + config.onUpdate, + ] + ); } export function getHandler( diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts index 9d401e6a6c..5617dbb2c0 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts @@ -27,7 +27,10 @@ export function isNativeEvent( export function maybeExtractNativeEvent( event: GestureHandlerEventWithHandlerData -) { +): + | GestureTouchEvent + | GestureUpdateEventWithHandlerData + | GestureStateChangeEventWithHandlerData { 'worklet'; return isNativeEvent(event) ? event.nativeEvent : event; From 913697f416ff1ae1a1092e9370d1be1e09c1312b Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 1 Dec 2025 13:30:47 +0100 Subject: [PATCH 2/8] Don't modify memoized halnders --- .../hooks/callbacks/useReanimatedEventHandler.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts index ee236934ae..469d4fca5a 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts @@ -9,20 +9,27 @@ import { } from '../../types'; import { stateMachine } from './stateMachine'; +const workletNOOP = () => { + 'worklet'; + // no-op +}; + export function useReanimatedEventHandler( handlerTag: number, handlers: GestureCallbacks, reanimatedHandler: ReanimatedHandler | undefined, changeEventCalculator: ChangeCalculatorType | undefined ) { + let workletizedHandlers = handlers; + // We don't want to call hooks conditionally, `useEvent` will be always called. // The only difference is whether we will send events to Reanimated or not. // The problem here is that if someone passes `Animated.event` as `onUpdate` prop, // it won't be workletized and therefore `useHandler` will throw. In that case we override it to empty `worklet`. - if (!Reanimated?.isWorkletFunction(handlers.onUpdate)) { - handlers.onUpdate = () => { - 'worklet'; - // no-op + if (!Reanimated?.isWorkletFunction(workletizedHandlers.onUpdate)) { + workletizedHandlers = { + ...workletizedHandlers, + onUpdate: workletNOOP, }; } From b9aa092a1eb7ba4305ff63297e8ca4b33cf9d9db Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 2 Dec 2025 08:46:57 +0100 Subject: [PATCH 3/8] Fix after merge --- .../src/v3/hooks/callbacks/stateMachine.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts index fd67e84476..4fb4c4a119 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts @@ -1,5 +1,5 @@ import { - flattenEvent, + flattenAndFilterEvent, isEventForHandlerWithTag, maybeExtractNativeEvent, runCallback, @@ -25,17 +25,18 @@ function handleStateChange( context: ReanimatedContext ) { 'worklet'; - const event = flattenEvent(eventWithData); + const { oldState, state } = eventWithData; + const event = flattenAndFilterEvent(eventWithData); - if (event.oldState === State.UNDETERMINED && event.state === State.BEGAN) { + if (oldState === State.UNDETERMINED && state === State.BEGAN) { runCallback(CALLBACK_TYPE.BEGAN, callbacks, event); } else if ( - (event.oldState === State.BEGAN || event.oldState === State.UNDETERMINED) && - event.state === State.ACTIVE + (oldState === State.BEGAN || oldState === State.UNDETERMINED) && + state === State.ACTIVE ) { runCallback(CALLBACK_TYPE.START, callbacks, event); - } else if (event.oldState !== event.state && event.state === State.END) { - if (event.oldState === State.ACTIVE) { + } else if (oldState !== state && state === State.END) { + if (oldState === State.ACTIVE) { runCallback(CALLBACK_TYPE.END, callbacks, event, true); } runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, true); @@ -44,10 +45,10 @@ function handleStateChange( context.lastUpdateEvent = undefined; } } else if ( - (event.state === State.FAILED || event.state === State.CANCELLED) && - event.state !== event.oldState + (state === State.FAILED || state === State.CANCELLED) && + state !== oldState ) { - if (event.oldState === State.ACTIVE) { + if (oldState === State.ACTIVE) { runCallback(CALLBACK_TYPE.END, callbacks, event, false); } runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, false); @@ -72,7 +73,7 @@ export function handleUpdate( ) : eventWithData; - const event = flattenEvent(eventWithChanges); + const event = flattenAndFilterEvent(eventWithChanges); // This should never happen, but since we don't want to call hooks conditionally, we have to mark // context as possibly undefined to make TypeScript happy. From c72fe9fa3a3a4e4fbacb78129cb4041c5c2b9c6e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 2 Dec 2025 08:50:26 +0100 Subject: [PATCH 4/8] Memoize modified handlers --- .../callbacks/useReanimatedEventHandler.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts index 469d4fca5a..537eec3301 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Reanimated, ReanimatedHandler, @@ -20,18 +21,20 @@ export function useReanimatedEventHandler( reanimatedHandler: ReanimatedHandler | undefined, changeEventCalculator: ChangeCalculatorType | undefined ) { - let workletizedHandlers = handlers; + const workletizedHandlers = useMemo(() => { + // We don't want to call hooks conditionally, `useEvent` will be always called. + // The only difference is whether we will send events to Reanimated or not. + // The problem here is that if someone passes `Animated.event` as `onUpdate` prop, + // it won't be workletized and therefore `useHandler` will throw. In that case we override it to empty `worklet`. + if (!Reanimated?.isWorkletFunction(handlers.onUpdate)) { + return { + ...handlers, + onUpdate: workletNOOP, + }; + } - // We don't want to call hooks conditionally, `useEvent` will be always called. - // The only difference is whether we will send events to Reanimated or not. - // The problem here is that if someone passes `Animated.event` as `onUpdate` prop, - // it won't be workletized and therefore `useHandler` will throw. In that case we override it to empty `worklet`. - if (!Reanimated?.isWorkletFunction(workletizedHandlers.onUpdate)) { - workletizedHandlers = { - ...workletizedHandlers, - onUpdate: workletNOOP, - }; - } + return handlers; + }, [handlers]); const callback = ( event: UnpackedGestureHandlerEventWithHandlerData @@ -40,7 +43,7 @@ export function useReanimatedEventHandler( stateMachine( handlerTag, event, - handlers, + workletizedHandlers, changeEventCalculator, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain reanimatedHandler?.context!, From 9f1c87f3ca7bf44d943ac2205ae196a56d8a565c Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 2 Dec 2025 08:51:51 +0100 Subject: [PATCH 5/8] config -> callbacks --- .../src/v3/hooks/utils/eventHandlersUtils.ts | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts index 9ce9119f30..2d3fab7cf5 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts @@ -4,32 +4,38 @@ import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; import { GestureCallbacks, UnpackedGestureHandlerEvent } from '../../types'; export function useMemoizedGestureCallbacks( - config: GestureCallbacks + callbacks: GestureCallbacks ): GestureCallbacks { return useMemo( () => ({ - ...(config.onBegin ? { onBegin: config.onBegin } : {}), - ...(config.onActivate ? { onActivate: config.onActivate } : {}), - ...(config.onDeactivate ? { onDeactivate: config.onDeactivate } : {}), - ...(config.onFinalize ? { onFinalize: config.onFinalize } : {}), - ...(config.onUpdate ? { onUpdate: config.onUpdate } : {}), - ...(config.onTouchesDown ? { onTouchesDown: config.onTouchesDown } : {}), - ...(config.onTouchesMove ? { onTouchesMove: config.onTouchesMove } : {}), - ...(config.onTouchesUp ? { onTouchesUp: config.onTouchesUp } : {}), - ...(config.onTouchesCancel - ? { onTouchesCancel: config.onTouchesCancel } + ...(callbacks.onBegin ? { onBegin: callbacks.onBegin } : {}), + ...(callbacks.onActivate ? { onActivate: callbacks.onActivate } : {}), + ...(callbacks.onDeactivate + ? { onDeactivate: callbacks.onDeactivate } + : {}), + ...(callbacks.onFinalize ? { onFinalize: callbacks.onFinalize } : {}), + ...(callbacks.onUpdate ? { onUpdate: callbacks.onUpdate } : {}), + ...(callbacks.onTouchesDown + ? { onTouchesDown: callbacks.onTouchesDown } + : {}), + ...(callbacks.onTouchesMove + ? { onTouchesMove: callbacks.onTouchesMove } + : {}), + ...(callbacks.onTouchesUp ? { onTouchesUp: callbacks.onTouchesUp } : {}), + ...(callbacks.onTouchesCancel + ? { onTouchesCancel: callbacks.onTouchesCancel } : {}), }), [ - config.onActivate, - config.onBegin, - config.onDeactivate, - config.onFinalize, - config.onTouchesCancel, - config.onTouchesDown, - config.onTouchesMove, - config.onTouchesUp, - config.onUpdate, + callbacks.onActivate, + callbacks.onBegin, + callbacks.onDeactivate, + callbacks.onFinalize, + callbacks.onTouchesCancel, + callbacks.onTouchesDown, + callbacks.onTouchesMove, + callbacks.onTouchesUp, + callbacks.onUpdate, ] ); } From 30dfc3c894dacd0d5fbdf789b5a19f804d8bf0d1 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 2 Dec 2025 08:52:46 +0100 Subject: [PATCH 6/8] Remove assignment --- .../src/v3/hooks/useGesture.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts index 4e8af7ee99..3f0bdbee05 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -42,17 +42,18 @@ export function useGesture( throw new Error(tagMessage('Failed to create reanimated event handlers.')); } - const gestureRelations = useMemo(() => { - const rel = prepareRelations( - { - simultaneousWith: config.simultaneousWith, - requireToFail: config.requireToFail, - block: config.block, - }, - tag - ); - return rel; - }, [tag, config.simultaneousWith, config.requireToFail, config.block]); + const gestureRelations = useMemo( + () => + prepareRelations( + { + simultaneousWith: config.simultaneousWith, + requireToFail: config.requireToFail, + block: config.block, + }, + tag + ), + [tag, config.simultaneousWith, config.requireToFail, config.block] + ); const currentGestureRef = useRef({ type: '', tag: -1 }); if ( From 527e417a07e4dfec6ce6e3d7f28494d136e0f01b Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 2 Dec 2025 14:55:40 +0100 Subject: [PATCH 7/8] Rename stateMachine -> eventHandler --- .../v3/hooks/callbacks/{stateMachine.ts => eventHandler.ts} | 2 +- .../src/v3/hooks/callbacks/useGestureEventHandler.ts | 4 ++-- .../src/v3/hooks/callbacks/useReanimatedEventHandler.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/react-native-gesture-handler/src/v3/hooks/callbacks/{stateMachine.ts => eventHandler.ts} (98%) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts similarity index 98% rename from packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts rename to packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts index 4fb4c4a119..1dbba20547 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateMachine.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts @@ -97,7 +97,7 @@ export function handleTouchEvent( } } -export function stateMachine( +export function eventHandler( handlerTag: number, sourceEvent: GestureHandlerEventWithHandlerData, handlers: GestureCallbacks, diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts index 827a8c5a63..442b0207cf 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts @@ -5,7 +5,7 @@ import { GestureHandlerEventWithHandlerData, } from '../../types'; import { useMemo } from 'react'; -import { stateMachine } from './stateMachine'; +import { eventHandler } from './eventHandler'; export function useGestureEventHandler( handlerTag: number, @@ -20,7 +20,7 @@ export function useGestureEventHandler( return useMemo(() => { return (event: GestureHandlerEventWithHandlerData) => { - stateMachine( + eventHandler( handlerTag, event, handlers, diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts index 537eec3301..012d18d3dc 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts @@ -8,7 +8,7 @@ import { GestureCallbacks, UnpackedGestureHandlerEventWithHandlerData, } from '../../types'; -import { stateMachine } from './stateMachine'; +import { eventHandler } from './eventHandler'; const workletNOOP = () => { 'worklet'; @@ -40,7 +40,7 @@ export function useReanimatedEventHandler( event: UnpackedGestureHandlerEventWithHandlerData ) => { 'worklet'; - stateMachine( + eventHandler( handlerTag, event, workletizedHandlers, From 47b29ed3ae754263bdd1173af5bae8b17f6377f5 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 3 Dec 2025 09:16:39 +0100 Subject: [PATCH 8/8] Rename methods --- .../src/v3/hooks/callbacks/eventHandler.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts index 1dbba20547..b01b90fd84 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts @@ -19,7 +19,7 @@ import { State } from '../../../State'; import { TouchEventType } from '../../../TouchEventType'; import { GestureTouchEvent } from '../../../handlers/gestureHandlerCommon'; -function handleStateChange( +function handleStateChangeEvent( eventWithData: GestureStateChangeEventWithHandlerData, callbacks: GestureCallbacks, context: ReanimatedContext @@ -59,7 +59,7 @@ function handleStateChange( } } -export function handleUpdate( +export function handleUpdateEvent( eventWithData: GestureUpdateEventWithHandlerData, handlers: GestureCallbacks, changeEventCalculator: ChangeCalculatorType | undefined, @@ -113,10 +113,15 @@ export function eventHandler( } if ('oldState' in eventWithData && eventWithData.oldState !== undefined) { - handleStateChange(eventWithData, handlers, jsContext); + handleStateChangeEvent(eventWithData, handlers, jsContext); } else if ('allTouches' in eventWithData) { handleTouchEvent(eventWithData, handlers); } else if (!dispatchesAnimatedEvents) { - handleUpdate(eventWithData, handlers, changeEventCalculator, jsContext); + handleUpdateEvent( + eventWithData, + handlers, + changeEventCalculator, + jsContext + ); } }