From adf65478b8d17fee7c5f79ad971e7b0b20ec7623 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 19 Nov 2025 11:37:56 +0100 Subject: [PATCH 1/6] Rename to `isNativeAnimatedEvent` --- .../src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts | 4 ++-- .../src/v3/hooks/useGestureCallbacks.ts | 7 +++++-- .../src/v3/hooks/utils/configUtils.ts | 4 ++-- .../src/v3/hooks/utils/eventUtils.ts | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) 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 index c40dcf9402..972e2c0ba1 100644 --- 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 @@ -1,4 +1,4 @@ -import { prepareUpdateHandlers, isAnimatedEvent } from '../../utils'; +import { prepareUpdateHandlers, isNativeAnimatedEvent } from '../../utils'; import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper'; import { getUpdateHandler } from '../updateHandler'; import { BaseGestureConfig } from '../../../types'; @@ -20,7 +20,7 @@ export function useGestureUpdateEvent( lastUpdateEvent: undefined, }; - return isAnimatedEvent(config.onUpdate) + return isNativeAnimatedEvent(config.onUpdate) ? undefined : getUpdateHandler( handlerTag, 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 b4ccedfd9a..9ef7c4970c 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts @@ -2,7 +2,10 @@ import { useGestureStateChangeEvent } from './callbacks/js/useGestureStateChange import { useGestureUpdateEvent } from './callbacks/js/useGestureUpdateEvent'; import { useGestureTouchEvent } from './callbacks/js/useGestureTouchEvent'; import { AnimatedEvent, BaseGestureConfig } from '../types'; -import { checkMappingForChangeProperties, isAnimatedEvent } from './utils'; +import { + checkMappingForChangeProperties, + isNativeAnimatedEvent, +} from './utils'; import { useReanimatedStateChangeEvent } from './callbacks/reanimated/useReanimatedStateChangeEvent'; import { useReanimatedUpdateEvent } from './callbacks/reanimated/useReanimatedUpdateEvent'; import { useReanimatedTouchEvent } from './callbacks/reanimated/useReanimatedTouchEvent'; @@ -32,7 +35,7 @@ export function useGestureCallbacks( } let onGestureHandlerAnimatedEvent: AnimatedEvent | undefined; - if (isAnimatedEvent(config.onUpdate)) { + if (isNativeAnimatedEvent(config.onUpdate)) { checkMappingForChangeProperties(config.onUpdate); onGestureHandlerAnimatedEvent = config.onUpdate; } diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts index a86ed018fe..b7efc59654 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts @@ -6,7 +6,7 @@ import { SingleGestureName, } from '../../types'; import { hasWorkletEventHandlers, maybeUnpackValue } from './reanimatedUtils'; -import { isAnimatedEvent, shouldHandleTouchEvents } from './eventUtils'; +import { isNativeAnimatedEvent, shouldHandleTouchEvents } from './eventUtils'; import { allowedNativeProps, EMPTY_WHITE_LIST, @@ -25,7 +25,7 @@ export function prepareConfig( Reanimated !== undefined && hasWorkletEventHandlers(config); config.needsPointerData = shouldHandleTouchEvents(config); - config.dispatchesAnimatedEvents = isAnimatedEvent(config.onUpdate); + config.dispatchesAnimatedEvents = isNativeAnimatedEvent(config.onUpdate); config.dispatchesReanimatedEvents = config.shouldUseReanimatedDetector && !runOnJS; } 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 0fc4e063c7..f31b5752cb 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 @@ -40,7 +40,7 @@ export function isEventForHandlerWithTag( return event.handlerTag === handlerTag; } -export function isAnimatedEvent( +export function isNativeAnimatedEvent( callback: | ((event: GestureUpdateEvent) => void) | AnimatedEvent From 5e2c1348da6e67431a81c98da91e41b6be988843 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 19 Nov 2025 12:08:57 +0100 Subject: [PATCH 2/6] Allow explicit opt-in for Animated --- .../src/v3/detectors/NativeDetector.tsx | 1 + .../InterceptingGestureDetector.tsx | 6 +++-- .../callbacks/js/useGestureUpdateEvent.ts | 11 ++++++--- .../src/v3/hooks/useGestureCallbacks.ts | 13 ++++++---- .../src/v3/hooks/utils/configUtils.ts | 24 +++++++++++++++++-- .../src/v3/hooks/utils/propsWhiteList.ts | 2 ++ .../src/v3/types/ConfigTypes.ts | 5 +++- .../src/v3/types/DetectorTypes.ts | 6 ++++- .../src/v3/types/EventTypes.ts | 8 +++---- 9 files changed, 59 insertions(+), 17 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx index 69360e40b0..385cb2f83a 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx @@ -50,6 +50,7 @@ export function NativeDetector({ onGestureHandlerReanimatedTouchEvent={ gesture.detectorCallbacks.onReanimatedTouchEvent } + // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerAnimatedEvent={ gesture.detectorCallbacks.onGestureHandlerAnimatedEvent } diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx index e8ae57a345..13feb4822e 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx @@ -126,13 +126,15 @@ export function InterceptingGestureDetector({ const createGestureEventHandler = useCallback( (key: keyof DetectorCallbacks) => { return (e: GestureHandlerEvent) => { - if (gesture?.detectorCallbacks[key]) { + if (typeof gesture?.detectorCallbacks[key] === 'function') { + // @ts-expect-error passing event to a union of functions where only one is typed as such gesture.detectorCallbacks[key](e); } virtualChildren.forEach((child) => { const method = child.methods[key]; - if (method) { + if (typeof method === 'function') { + // @ts-expect-error passing event to a union of functions where only one is typed as such method(e); } }); 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 index 972e2c0ba1..8fdaf6b2de 100644 --- 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 @@ -1,4 +1,4 @@ -import { prepareUpdateHandlers, isNativeAnimatedEvent } from '../../utils'; +import { prepareUpdateHandlers } from '../../utils'; import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper'; import { getUpdateHandler } from '../updateHandler'; import { BaseGestureConfig } from '../../../types'; @@ -20,7 +20,7 @@ export function useGestureUpdateEvent( lastUpdateEvent: undefined, }; - return isNativeAnimatedEvent(config.onUpdate) + return config.dispatchesAnimatedEvents ? undefined : getUpdateHandler( handlerTag, @@ -28,5 +28,10 @@ export function useGestureUpdateEvent( jsContext, changeEventCalculator ); - }, [handlerTag, config.onUpdate, config.changeEventCalculator]); + }, [ + handlerTag, + config.onUpdate, + config.dispatchesAnimatedEvents, + config.changeEventCalculator, + ]); } 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 9ef7c4970c..2051c20111 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts @@ -1,7 +1,7 @@ import { useGestureStateChangeEvent } from './callbacks/js/useGestureStateChangeEvent'; import { useGestureUpdateEvent } from './callbacks/js/useGestureUpdateEvent'; import { useGestureTouchEvent } from './callbacks/js/useGestureTouchEvent'; -import { AnimatedEvent, BaseGestureConfig } from '../types'; +import { AnimatedEvent, BaseGestureConfig, GestureUpdateEvent } from '../types'; import { checkMappingForChangeProperties, isNativeAnimatedEvent, @@ -34,9 +34,14 @@ export function useGestureCallbacks( onReanimatedTouchEvent = useReanimatedTouchEvent(handlerTag, config); } - let onGestureHandlerAnimatedEvent: AnimatedEvent | undefined; - if (isNativeAnimatedEvent(config.onUpdate)) { - checkMappingForChangeProperties(config.onUpdate); + let onGestureHandlerAnimatedEvent: + | ((event: GestureUpdateEvent) => void) + | AnimatedEvent + | undefined; + if (config.dispatchesAnimatedEvents) { + if (__DEV__ && isNativeAnimatedEvent(config.onUpdate)) { + checkMappingForChangeProperties(config.onUpdate); + } onGestureHandlerAnimatedEvent = config.onUpdate; } diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts index b7efc59654..9957145713 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts @@ -20,12 +20,32 @@ export function prepareConfig( ) { const runOnJS = maybeUnpackValue(config.runOnJS); + config.dispatchesAnimatedEvents = + config.useAnimated || isNativeAnimatedEvent(config.onUpdate); + + // Validate that the user is not trying to mix Animated and Reanimated before updating the config. + if ( + __DEV__ && + config.dispatchesAnimatedEvents && + config.disableReanimated === false + ) { + throw new Error( + tagMessage( + 'Animated cannot be used together with Reanimated in the same gesture. Please choose either Animated or Reanimated for handling gesture events.' + ) + ); + } + + if (config.dispatchesAnimatedEvents) { + config.disableReanimated = true; + } + config.shouldUseReanimatedDetector = !config.disableReanimated && Reanimated !== undefined && - hasWorkletEventHandlers(config); + hasWorkletEventHandlers(config) && + !config.dispatchesAnimatedEvents; config.needsPointerData = shouldHandleTouchEvents(config); - config.dispatchesAnimatedEvents = isNativeAnimatedEvent(config.onUpdate); config.dispatchesReanimatedEvents = config.shouldUseReanimatedDetector && !runOnJS; } diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts index 1091cbe8ce..0667914bc3 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts @@ -53,6 +53,8 @@ export const PropsToFilter = new Set>([ 'changeEventCalculator', 'disableReanimated', 'shouldUseReanimatedDetector', + 'useAnimated', + 'runOnJS', // Relations 'simultaneousWithExternalGesture', diff --git a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts index 9d8b244495..5167f160a4 100644 --- a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts @@ -25,7 +25,9 @@ export type GestureCallbacks = { event: GestureStateChangeEvent, didSucceed: boolean ) => void; - onUpdate?: (event: GestureUpdateEvent) => void | AnimatedEvent; + onUpdate?: + | ((event: GestureUpdateEvent) => void) + | AnimatedEvent; onTouchesDown?: (event: GestureTouchEvent) => void; onTouchesMove?: (event: GestureTouchEvent) => void; onTouchesUp?: (event: GestureTouchEvent) => void; @@ -48,6 +50,7 @@ export type InternalConfigProps = { export type CommonGestureConfig = { disableReanimated?: boolean; + useAnimated?: boolean; } & WithSharedValue< { runOnJS?: boolean; diff --git a/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts b/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts index e7416b24ba..1f83680e10 100644 --- a/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts @@ -3,6 +3,7 @@ import { StateChangeEvent, UpdateEvent, TouchEvent, + GestureUpdateEvent, } from './EventTypes'; export type DetectorCallbacks = { @@ -18,7 +19,10 @@ export type DetectorCallbacks = { | undefined | ((event: UpdateEvent) => void); onReanimatedTouchEvent: undefined | ((event: TouchEvent) => void); - onGestureHandlerAnimatedEvent: undefined | AnimatedEvent; + onGestureHandlerAnimatedEvent: + | undefined + | AnimatedEvent + | ((event: GestureUpdateEvent) => void); }; export type VirtualChild = { diff --git a/packages/react-native-gesture-handler/src/v3/types/EventTypes.ts b/packages/react-native-gesture-handler/src/v3/types/EventTypes.ts index 62e95994be..cb28a0af4b 100644 --- a/packages/react-native-gesture-handler/src/v3/types/EventTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/EventTypes.ts @@ -48,10 +48,10 @@ export type TouchEvent = | GestureTouchEvent | NativeSyntheticEvent; -// This is almost how Animated.event is typed in React Native. We add _argMapping in order to: -// 1. Distinguish it from a regular function, -// 2. Have access to the _argMapping property to check for usage of `change*` callbacks. -export type AnimatedEvent = ((...args: any[]) => void) & { +// This is not how Animated.event is typed in React Native. We add _argMapping in order to +// have access to the _argMapping property to check for usage of `change*` callbacks. +// It's also not typed as a function, which is breaking Gesture Handler type definitions. +export type AnimatedEvent = { _argMapping: (Animated.Mapping | null)[]; }; From 39706b9f652806f1ac21f1e1fc2de2ca81158a11 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 19 Nov 2025 12:28:04 +0100 Subject: [PATCH 3/6] Catch invalid event mapping and rethrow --- .../src/v3/hooks/useGestureCallbacks.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) 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 2051c20111..b6895e8f09 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts @@ -9,6 +9,30 @@ import { import { useReanimatedStateChangeEvent } from './callbacks/reanimated/useReanimatedStateChangeEvent'; import { useReanimatedUpdateEvent } from './callbacks/reanimated/useReanimatedUpdateEvent'; import { useReanimatedTouchEvent } from './callbacks/reanimated/useReanimatedTouchEvent'; +import { tagMessage } from '../../utils'; + +function guardJSAnimatedEvent(handler: (...args: unknown[]) => void) { + return (...args: unknown[]) => { + try { + handler(...args); + } catch (e) { + if ( + e instanceof Error && + e.message.includes('Bad event of type undefined for key') + ) { + throw new Error( + tagMessage( + 'The event mapping inside an Animated.event is invalid. ' + + 'Please make sure you are using the correct structure for the gesture event:\n\n' + + '{ nativeEvent: { handlerData: { /* your mappings here */ } } }' + ) + ); + } + + throw e; + } + }; +} export function useGestureCallbacks( handlerTag: number, @@ -42,7 +66,13 @@ export function useGestureCallbacks( if (__DEV__ && isNativeAnimatedEvent(config.onUpdate)) { checkMappingForChangeProperties(config.onUpdate); } - onGestureHandlerAnimatedEvent = config.onUpdate; + + if (__DEV__ && !isNativeAnimatedEvent(config.onUpdate)) { + // @ts-expect-error At this point we know it's not a native animated event, so it's callable + onGestureHandlerAnimatedEvent = guardJSAnimatedEvent(config.onUpdate); + } else { + onGestureHandlerAnimatedEvent = config.onUpdate; + } } return { From 1f8f7fd87ac537c91b7127833d2052b702f11400 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 19 Nov 2025 12:42:04 +0100 Subject: [PATCH 4/6] Warn when using native Animated.Event without opt-in --- .../src/v3/hooks/utils/configUtils.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts index 9957145713..9169814a68 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts @@ -20,6 +20,20 @@ export function prepareConfig( ) { const runOnJS = maybeUnpackValue(config.runOnJS); + if ( + __DEV__ && + isNativeAnimatedEvent(config.onUpdate) && + !config.useAnimated + ) { + console.warn( + tagMessage( + 'You are using Animated.event in onUpdate without setting useAnimated to true. ' + + 'This may lead to unexpected behavior. If you intend to use Animated.event, ' + + 'please set useAnimated to true in the gesture config.' + ) + ); + } + config.dispatchesAnimatedEvents = config.useAnimated || isNativeAnimatedEvent(config.onUpdate); From e791e9d8fb6f5a77924b583c898c8acde197b0a0 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 19 Nov 2025 13:39:56 +0100 Subject: [PATCH 5/6] Update error condition --- .../src/v3/hooks/utils/configUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts index 9169814a68..8ddf111c23 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/configUtils.ts @@ -41,7 +41,7 @@ export function prepareConfig( if ( __DEV__ && config.dispatchesAnimatedEvents && - config.disableReanimated === false + (config.disableReanimated === false || config.runOnJS === false) ) { throw new Error( tagMessage( From 0d0c5054f7069c1d1f908fc9ffb0c8e921023e46 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 19 Nov 2025 13:40:20 +0100 Subject: [PATCH 6/6] Remove unreachable error --- .../src/v3/hooks/useGesture.ts | 8 -------- 1 file changed, 8 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 1b9bbf9eff..019141082f 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -30,14 +30,6 @@ export function useGesture( // This has to be done ASAP as other hooks depend `shouldUseReanimatedDetector`. prepareConfig(config); - if (config.dispatchesAnimatedEvents && config.shouldUseReanimatedDetector) { - throw new Error( - tagMessage( - `${type}: You cannot use Animated.Event together with callbacks running on the UI thread. Either remove Animated.Event from onUpdate, or set runOnJS property to true on the gesture.` - ) - ); - } - // TODO: Call only necessary hooks depending on which callbacks are defined (?) const { onGestureHandlerStateChange,