From 86423e118bb9d32d48028e350d473019678c2389 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 12 Nov 2025 13:52:06 +0100 Subject: [PATCH 01/16] Memoze event handlers --- .../js/useGestureStateChangeEvent.ts | 8 +-- .../callbacks/js/useGestureTouchEvent.ts | 8 +-- .../callbacks/js/useGestureUpdateEvent.ts | 22 ++++++--- .../src/v3/hooks/useGesture.ts | 49 ++++++++++++++----- 4 files changed, 61 insertions(+), 26 deletions(-) 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 index d0832597f1..16a8ba4b4b 100644 --- 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 @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { BaseGestureConfig } from '../../../types'; import { extractStateChangeHandlers } from '../../utils'; import { getStateChangeHandler } from '../stateChangeHandler'; @@ -6,7 +7,8 @@ export function useGestureStateChangeEvent( handlerTag: number, config: BaseGestureConfig ) { - const handlers = extractStateChangeHandlers(config); - - return getStateChangeHandler(handlerTag, handlers); + return useMemo(() => { + const handlers = extractStateChangeHandlers(config); + return getStateChangeHandler(handlerTag, handlers); + }, [handlerTag, config]); } 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 index f7eea78714..92ef9b06f6 100644 --- 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 @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { BaseGestureConfig } from '../../../types'; import { extractTouchHandlers } from '../../utils'; import { getTouchEventHandler } from '../touchEventHandler'; @@ -6,7 +7,8 @@ export function useGestureTouchEvent( handlerTag: number, config: BaseGestureConfig ) { - const handlers = extractTouchHandlers(config); - - return getTouchEventHandler(handlerTag, handlers); + return useMemo(() => { + const handlers = extractTouchHandlers(config); + return getTouchEventHandler(handlerTag, handlers); + }, [handlerTag, config]); } 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 6179273192..5219c1cc3f 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 @@ -2,18 +2,26 @@ import { extractUpdateHandlers, isAnimatedEvent } 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 ) { - const { handlers, changeEventCalculator } = extractUpdateHandlers(config); + return useMemo(() => { + const { handlers, changeEventCalculator } = extractUpdateHandlers(config); - const jsContext: ReanimatedContext = { - lastUpdateEvent: undefined, - }; + const jsContext: ReanimatedContext = { + lastUpdateEvent: undefined, + }; - return isAnimatedEvent(config.onUpdate) - ? undefined - : getUpdateHandler(handlerTag, handlers, jsContext, changeEventCalculator); + return isAnimatedEvent(config.onUpdate) + ? undefined + : getUpdateHandler( + handlerTag, + handlers, + jsContext, + changeEventCalculator + ); + }, [handlerTag, config]); } 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 6a641d2b05..50ae603d3a 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { getNextHandlerTag } from '../../handlers/getNextHandlerTag'; import RNGestureHandlerModule from '../../RNGestureHandlerModule'; import { useGestureCallbacks } from './useGestureCallbacks'; @@ -69,12 +69,20 @@ export function useGesture( throw new Error(tagMessage('Failed to create reanimated event handlers.')); } - const gestureRelations = prepareRelations(config, tag); + const gestureRelations = useMemo( + () => prepareRelations(config, tag), + [config, tag] + ); - useMemo(() => { + const currentGestureRef = useRef({ type: '', tag: -1 }); + if ( + currentGestureRef.current.tag !== tag || + currentGestureRef.current.type !== (type as string) + ) { + currentGestureRef.current = { type, tag }; RNGestureHandlerModule.createGestureHandler(type, tag, {}); RNGestureHandlerModule.flushOperations(); - }, [type, tag]); + } useEffect(() => { return () => { @@ -93,13 +101,28 @@ export function useGesture( return () => { unbindSharedValues(config, tag); }; - }, [tag, config]); + }, [tag, config, type]); - return { - tag, - type, - config, - detectorCallbacks: { + return useMemo( + () => ({ + tag, + type, + config, + detectorCallbacks: { + onGestureHandlerStateChange, + onGestureHandlerEvent, + onGestureHandlerTouchEvent, + onReanimatedStateChange, + onReanimatedUpdateEvent, + onReanimatedTouchEvent, + onGestureHandlerAnimatedEvent, + }, + gestureRelations, + }), + [ + tag, + type, + config, onGestureHandlerStateChange, onGestureHandlerEvent, onGestureHandlerTouchEvent, @@ -107,7 +130,7 @@ export function useGesture( onReanimatedUpdateEvent, onReanimatedTouchEvent, onGestureHandlerAnimatedEvent, - }, - gestureRelations, - }; + gestureRelations, + ] + ); } From 9700adf312b7b622601c11632d980fd3130eab23 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 12 Nov 2025 15:44:04 +0100 Subject: [PATCH 02/16] Simplify ref handling --- .../VirtualDetector/VirtualDetector.tsx | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index 7e7e486ecb..59d564e7ff 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -11,6 +11,10 @@ import { DetectorCallbacks } from '../../types'; export function VirtualDetector( props: NativeDetectorProps ) { + // Don't memoize virtual detectors to be able to listen to changes in children + // TODO: replace with MutationObserver when it rolls out in React Native + 'use no memo'; + const context = useDetectorContext(); if (!context) { throw new Error( @@ -29,26 +33,15 @@ export function VirtualDetector( const handleRef = useCallback( (node: any) => { viewRef.current = node; - if (!node) { - return; - } - - const tag = Platform.OS === 'web' ? node : findNodeHandle(node); - - if (tag != null) { - setViewTag(tag); + if (node) { + const tag: number = Platform.OS === 'web' ? node : findNodeHandle(node); + setViewTag(tag ?? -1); + } else { + setViewTag(-1); } - - return () => { - if (tag != null) { - const handlerTags = isComposedGesture(props.gesture) - ? props.gesture.tags - : [props.gesture.tag]; - - unregister(tag, handlerTags); - } - }; }, + // Invalid dependency array to change the function when children change + // eslint-disable-next-line react-hooks/exhaustive-deps [props.children] ); From 62e2a1a89f08e8750d1e52095da424b6dc378633 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 12 Nov 2025 16:04:07 +0100 Subject: [PATCH 03/16] Remove ref --- .../InterceptingGestureDetector.tsx | 20 ++++++++++--------- .../VirtualDetector/VirtualDetector.tsx | 12 ++++------- .../VirtualDetector/useDetectorContext.ts | 4 ++-- 3 files changed, 17 insertions(+), 19 deletions(-) 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 83ec28be86..34dba74cfb 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 @@ -1,4 +1,4 @@ -import React, { RefObject, useCallback, useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import HostGestureDetector from '../HostGestureDetector'; import { VirtualChildren, @@ -23,9 +23,9 @@ export function InterceptingGestureDetector({ }: InterceptingGestureDetectorProps) { const [virtualChildren, setVirtualChildren] = useState([]); - const virtualMethods = useRef< - Map>> - >(new Map()); + const virtualMethods = useRef>>( + new Map() + ); const [shouldUseReanimated, setShouldUseReanimated] = useState( gesture ? gesture.config.shouldUseReanimatedDetector : false @@ -43,10 +43,11 @@ export function InterceptingGestureDetector({ const register = useCallback( ( child: VirtualChildren, - methods: RefObject>, + methods: DetectorCallbacks, forReanimated: boolean | undefined, forAnimated: boolean | undefined ) => { + // console.log('[IGD] Registering virtual detector', child.viewTag, child.handlerTags); setShouldUseReanimated(!!forReanimated); setDispatchesAnimatedEvents(!!forAnimated); @@ -69,6 +70,7 @@ export function InterceptingGestureDetector({ ); const unregister = useCallback((childTag: number, handlerTags: number[]) => { + // console.log('[IGD] Unregistering virtual detector', childTag, handlerTags); handlerTags.forEach((tag) => { virtualMethods.current.delete(tag); }); @@ -91,8 +93,8 @@ export function InterceptingGestureDetector({ gesture.detectorCallbacks[key](e); } - virtualMethods.current.forEach((ref) => { - const method = ref.current?.[key]; + virtualMethods.current.forEach((callbacks) => { + const method = callbacks[key]; if (method) { method(e); } @@ -112,8 +114,8 @@ export function InterceptingGestureDetector({ ); } - virtualMethods.current.forEach((ref) => { - const handler = ref.current?.[key]; + virtualMethods.current.forEach((callbacks) => { + const handler = callbacks[key]; if (handler) { handlers.push( handler as (e: GestureHandlerEvent) => void diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index 59d564e7ff..67cbf1628f 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -1,4 +1,4 @@ -import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Wrap } from '../../../handlers/gestures/GestureDetector/Wrap'; import { findNodeHandle, Platform } from 'react-native'; import { useDetectorContext } from './useDetectorContext'; @@ -28,8 +28,6 @@ export function VirtualDetector( const viewRef = useRef(null); const [viewTag, setViewTag] = useState(-1); - const virtualMethods = useRef(props.gesture.detectorCallbacks); - const handleRef = useCallback( (node: any) => { viewRef.current = node; @@ -45,10 +43,6 @@ export function VirtualDetector( [props.children] ); - useEffect(() => { - virtualMethods.current = props.gesture.detectorCallbacks; - }, [props.gesture.detectorCallbacks]); - useEffect(() => { if (viewTag === -1) { return; @@ -67,14 +61,16 @@ export function VirtualDetector( Object.assign(virtualProps, { viewRef }); } + console.log('[VD] Registering virtual detector', viewTag, handlerTags); register( virtualProps, - virtualMethods as RefObject>, + props.gesture.detectorCallbacks as DetectorCallbacks, props.gesture.config.shouldUseReanimatedDetector, props.gesture.config.dispatchesAnimatedEvents ); return () => { + console.log('[VD] Unregistering virtual detector', viewTag, handlerTags); unregister(viewTag, handlerTags); }; }, [viewTag, props.gesture, register, unregister]); diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts index 2eecca7129..de02344481 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts @@ -1,10 +1,10 @@ -import { createContext, RefObject, use } from 'react'; +import { createContext, use } from 'react'; import { DetectorCallbacks, VirtualChildren } from '../../types'; type DetectorContextType = { register: ( child: VirtualChildren, - methods: RefObject>, + methods: DetectorCallbacks, forReanimated: boolean | undefined, forAnimated: boolean | undefined ) => void; From ea3d0950149250dc7d6ded2aceb3ac4d9b1f6436 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 12 Nov 2025 16:15:46 +0100 Subject: [PATCH 04/16] Don't keep virtual callbacks in ref --- .../InterceptingGestureDetector.tsx | 80 ++++++++----------- .../VirtualDetector/VirtualDetector.tsx | 24 +++--- .../VirtualDetector/useDetectorContext.ts | 11 +-- .../src/v3/types/DetectorTypes.ts | 8 +- 4 files changed, 52 insertions(+), 71 deletions(-) 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 34dba74cfb..b375193125 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 @@ -1,7 +1,7 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import HostGestureDetector from '../HostGestureDetector'; import { - VirtualChildren, + VirtualChild, GestureHandlerEvent, DetectorCallbacks, } from '../../types'; @@ -21,17 +21,24 @@ export function InterceptingGestureDetector({ gesture, children, }: InterceptingGestureDetectorProps) { - const [virtualChildren, setVirtualChildren] = useState([]); - - const virtualMethods = useRef>>( - new Map() + const [virtualChildren, setVirtualChildren] = useState([]); + + const shouldUseReanimated = useMemo( + () => + virtualChildren.reduce( + (acc, child) => acc || child.forReanimated, + gesture?.config.shouldUseReanimatedDetector ?? false + ), + [virtualChildren, gesture] ); - const [shouldUseReanimated, setShouldUseReanimated] = useState( - gesture ? gesture.config.shouldUseReanimatedDetector : false - ); - const [dispatchesAnimatedEvents, setDispatchesAnimatedEvents] = useState( - gesture ? gesture.config.dispatchesAnimatedEvents : false + const dispatchesAnimatedEvents = useMemo( + () => + virtualChildren.reduce( + (acc, child) => acc || child.forAnimated, + gesture?.config.dispatchesAnimatedEvents ?? false + ), + [virtualChildren, gesture] ); const NativeDetectorComponent = dispatchesAnimatedEvents @@ -40,41 +47,20 @@ export function InterceptingGestureDetector({ ? ReanimatedNativeDetector : HostGestureDetector; - const register = useCallback( - ( - child: VirtualChildren, - methods: DetectorCallbacks, - forReanimated: boolean | undefined, - forAnimated: boolean | undefined - ) => { - // console.log('[IGD] Registering virtual detector', child.viewTag, child.handlerTags); - setShouldUseReanimated(!!forReanimated); - setDispatchesAnimatedEvents(!!forAnimated); - - setVirtualChildren((prev) => { - const index = prev.findIndex((c) => c.viewTag === child.viewTag); - if (index !== -1) { - const updated = [...prev]; - updated[index] = child; - return updated; - } - - return [...prev, child]; - }); - - child.handlerTags.forEach((tag) => { - virtualMethods.current.set(tag, methods); - }); - }, - [] - ); + const register = useCallback((child: VirtualChild) => { + setVirtualChildren((prev) => { + const index = prev.findIndex((c) => c.viewTag === child.viewTag); + if (index !== -1) { + const updated = [...prev]; + updated[index] = child; + return updated; + } - const unregister = useCallback((childTag: number, handlerTags: number[]) => { - // console.log('[IGD] Unregistering virtual detector', childTag, handlerTags); - handlerTags.forEach((tag) => { - virtualMethods.current.delete(tag); + return [...prev, child]; }); + }, []); + const unregister = useCallback((childTag: number) => { setVirtualChildren((prev) => prev.filter((c) => c.viewTag !== childTag)); }, []); @@ -93,8 +79,8 @@ export function InterceptingGestureDetector({ gesture.detectorCallbacks[key](e); } - virtualMethods.current.forEach((callbacks) => { - const method = callbacks[key]; + virtualChildren.forEach((child) => { + const method = child.methods[key]; if (method) { method(e); } @@ -114,8 +100,8 @@ export function InterceptingGestureDetector({ ); } - virtualMethods.current.forEach((callbacks) => { - const handler = callbacks[key]; + virtualChildren.forEach((child) => { + const handler = child.methods[key]; if (handler) { handlers.push( handler as (e: GestureHandlerEvent) => void diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index 67cbf1628f..9693ffe03e 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -6,7 +6,7 @@ import { isComposedGesture } from '../../hooks/utils/relationUtils'; import { NativeDetectorProps } from '../common'; import { configureRelations } from '../utils'; import { tagMessage } from '../../../utils'; -import { DetectorCallbacks } from '../../types'; +import { DetectorCallbacks, VirtualChild } from '../../types'; export function VirtualDetector( props: NativeDetectorProps @@ -52,26 +52,20 @@ export function VirtualDetector( ? props.gesture.tags : [props.gesture.tag]; - const virtualProps = { + const virtualChild: VirtualChild = { viewTag, handlerTags, + methods: props.gesture.detectorCallbacks as DetectorCallbacks, + forReanimated: !!props.gesture.config.shouldUseReanimatedDetector, + forAnimated: !!props.gesture.config.dispatchesAnimatedEvents, + // TODO: why do we need this? + viewRef: Platform.OS === 'web' ? viewRef.current : undefined, }; - if (Platform.OS === 'web') { - Object.assign(virtualProps, { viewRef }); - } - - console.log('[VD] Registering virtual detector', viewTag, handlerTags); - register( - virtualProps, - props.gesture.detectorCallbacks as DetectorCallbacks, - props.gesture.config.shouldUseReanimatedDetector, - props.gesture.config.dispatchesAnimatedEvents - ); + register(virtualChild); return () => { - console.log('[VD] Unregistering virtual detector', viewTag, handlerTags); - unregister(viewTag, handlerTags); + unregister(viewTag); }; }, [viewTag, props.gesture, register, unregister]); diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts index de02344481..7c7ad64c7e 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts @@ -1,14 +1,9 @@ import { createContext, use } from 'react'; -import { DetectorCallbacks, VirtualChildren } from '../../types'; +import { VirtualChild } from '../../types'; type DetectorContextType = { - register: ( - child: VirtualChildren, - methods: DetectorCallbacks, - forReanimated: boolean | undefined, - forAnimated: boolean | undefined - ) => void; - unregister: (child: number, handlerTags: number[]) => void; + register: (child: VirtualChild) => void; + unregister: (child: number) => void; }; export const DetectorContext = createContext(null); 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 a14fd365cc..18ab3953f9 100644 --- a/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts @@ -21,7 +21,13 @@ export type DetectorCallbacks = { onGestureHandlerAnimatedEvent: undefined | AnimatedEvent; }; -export type VirtualChildren = { +export type VirtualChild = { viewTag: number; handlerTags: number[]; + methods: DetectorCallbacks; + forReanimated: boolean; + forAnimated: boolean; + + // only set on web + viewRef: unknown; }; From 95b0d6fd4bf503875d094bbec93dda92432462dd Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 12 Nov 2025 16:22:27 +0100 Subject: [PATCH 05/16] Memo reanimated handlers --- .../InterceptingGestureDetector.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) 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 b375193125..03bdbb11da 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 @@ -114,14 +114,28 @@ export function InterceptingGestureDetector({ [virtualChildren, gesture?.detectorCallbacks] ); + const reanimatedUpdateEvents = useMemo( + () => getHandlers('onReanimatedUpdateEvent'), + [getHandlers] + ); const reanimatedEventHandler = Reanimated?.useComposedEventHandler( - getHandlers('onReanimatedUpdateEvent') + reanimatedUpdateEvents + ); + + const reanimatedStateChangeEvents = useMemo( + () => getHandlers('onReanimatedStateChange'), + [getHandlers] ); const reanimatedStateChangeHandler = Reanimated?.useComposedEventHandler( - getHandlers('onReanimatedStateChange') + reanimatedStateChangeEvents + ); + + const reanimatedTouchEvents = useMemo( + () => getHandlers('onReanimatedTouchEvent'), + [getHandlers] ); const reanimatedTouchEventHandler = Reanimated?.useComposedEventHandler( - getHandlers('onReanimatedTouchEvent') + reanimatedTouchEvents ); ensureNativeDetectorComponent(NativeDetectorComponent); From 22744bfe1673fc98b6587034b5132946a1ba273e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 13 Nov 2025 09:38:15 +0100 Subject: [PATCH 06/16] Fix `viewRef` passing --- .../src/v3/detectors/VirtualDetector/VirtualDetector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index 9693ffe03e..d8151261dc 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -58,8 +58,8 @@ export function VirtualDetector( methods: props.gesture.detectorCallbacks as DetectorCallbacks, forReanimated: !!props.gesture.config.shouldUseReanimatedDetector, forAnimated: !!props.gesture.config.dispatchesAnimatedEvents, - // TODO: why do we need this? - viewRef: Platform.OS === 'web' ? viewRef.current : undefined, + // used by HostGestureDetector on web + viewRef: Platform.OS === 'web' ? viewRef : undefined, }; register(virtualChild); From 8bde66d19f2eab98c79fd75e15b14f12aac49ee9 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 13 Nov 2025 11:02:10 +0100 Subject: [PATCH 07/16] Move validation to its own hook --- .../VirtualDetector/VirtualDetector.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index d8151261dc..53729c4318 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -8,13 +8,7 @@ import { configureRelations } from '../utils'; import { tagMessage } from '../../../utils'; import { DetectorCallbacks, VirtualChild } from '../../types'; -export function VirtualDetector( - props: NativeDetectorProps -) { - // Don't memoize virtual detectors to be able to listen to changes in children - // TODO: replace with MutationObserver when it rolls out in React Native - 'use no memo'; - +function useRequiredDetectorContext() { const context = useDetectorContext(); if (!context) { throw new Error( @@ -23,7 +17,17 @@ export function VirtualDetector( ) ); } - const { register, unregister } = context; + return context; +} + +export function VirtualDetector( + props: NativeDetectorProps +) { + // Don't memoize virtual detectors to be able to listen to changes in children + // TODO: replace with MutationObserver when it rolls out in React Native + 'use no memo'; + + const { register, unregister } = useRequiredDetectorContext(); const viewRef = useRef(null); const [viewTag, setViewTag] = useState(-1); From a8549b054bd3a77e2f9336799641e28223fb410b Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 13 Nov 2025 11:03:20 +0100 Subject: [PATCH 08/16] Keep intercepting gesture detector value stable --- .../VirtualDetector/InterceptingGestureDetector.tsx | 12 ++++++++++-- .../detectors/VirtualDetector/useDetectorContext.ts | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) 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 03bdbb11da..7e4d6a2f52 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 @@ -5,7 +5,7 @@ import { GestureHandlerEvent, DetectorCallbacks, } from '../../types'; -import { DetectorContext } from './useDetectorContext'; +import { DetectorContext, DetectorContextValue } from './useDetectorContext'; import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; import { configureRelations, ensureNativeDetectorComponent } from '../utils'; import { isComposedGesture } from '../../hooks/utils/relationUtils'; @@ -64,6 +64,14 @@ export function InterceptingGestureDetector({ setVirtualChildren((prev) => prev.filter((c) => c.viewTag !== childTag)); }, []); + const contextValue: DetectorContextValue = useMemo( + () => ({ + register, + unregister, + }), + [register, unregister] + ); + // It might happen only with ReanimatedNativeDetector if (!NativeDetectorComponent) { throw new Error( @@ -145,7 +153,7 @@ export function InterceptingGestureDetector({ } return ( - + void; unregister: (child: number) => void; }; -export const DetectorContext = createContext(null); +export const DetectorContext = createContext(null); export function useDetectorContext() { const ctx = use(DetectorContext); From 19ea1a9f368b8df8bb442249a44fefb1be3e91a9 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 13 Nov 2025 11:16:33 +0100 Subject: [PATCH 09/16] Memoize intercepting detector props --- .../InterceptingGestureDetector.tsx | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) 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 7e4d6a2f52..489e78581b 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 @@ -81,20 +81,23 @@ export function InterceptingGestureDetector({ ); } - const handleGestureEvent = (key: keyof DetectorCallbacks) => { - return (e: GestureHandlerEvent) => { - if (gesture?.detectorCallbacks[key]) { - gesture.detectorCallbacks[key](e); - } - - virtualChildren.forEach((child) => { - const method = child.methods[key]; - if (method) { - method(e); + const createGestureEventHandler = useCallback( + (key: keyof DetectorCallbacks) => { + return (e: GestureHandlerEvent) => { + if (gesture?.detectorCallbacks[key]) { + gesture.detectorCallbacks[key](e); } - }); - }; - }; + + virtualChildren.forEach((child) => { + const method = child.methods[key]; + if (method) { + method(e); + } + }); + }; + }, + [gesture, virtualChildren] + ); const getHandlers = useCallback( (key: keyof DetectorCallbacks) => { @@ -152,22 +155,34 @@ export function InterceptingGestureDetector({ configureRelations(gesture); } + const handlerTags = useMemo(() => { + if (gesture) { + return isComposedGesture(gesture) ? gesture.tags : [gesture.tag]; + } + return []; + }, [gesture]); + return ( createGestureEventHandler('onGestureHandlerStateChange'), + [createGestureEventHandler] )} // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types - onGestureHandlerEvent={handleGestureEvent('onGestureHandlerEvent')} + onGestureHandlerEvent={useMemo( + () => createGestureEventHandler('onGestureHandlerEvent'), + [createGestureEventHandler] + )} // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerAnimatedEvent={ gesture?.detectorCallbacks.onGestureHandlerAnimatedEvent } // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types - onGestureHandlerTouchEvent={handleGestureEvent( - 'onGestureHandlerTouchEvent' + onGestureHandlerTouchEvent={useMemo( + () => createGestureEventHandler('onGestureHandlerTouchEvent'), + [createGestureEventHandler] )} // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerReanimatedStateChange={ @@ -181,13 +196,7 @@ export function InterceptingGestureDetector({ onGestureHandlerReanimatedTouchEvent={ shouldUseReanimated ? reanimatedTouchEventHandler : undefined } - handlerTags={ - gesture - ? isComposedGesture(gesture) - ? gesture.tags - : [gesture.tag] - : [] - } + handlerTags={handlerTags} style={nativeDetectorStyles.detector} virtualChildren={virtualChildren} moduleId={globalThis._RNGH_MODULE_ID}> From 2403e0674f12124a859b0b85fac4175a68c519fe Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 14 Nov 2025 09:36:23 +0100 Subject: [PATCH 10/16] Rename variable --- .../VirtualDetector/InterceptingGestureDetector.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 489e78581b..7c14165960 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 @@ -23,7 +23,7 @@ export function InterceptingGestureDetector({ }: InterceptingGestureDetectorProps) { const [virtualChildren, setVirtualChildren] = useState([]); - const shouldUseReanimated = useMemo( + const shouldUseReanimatedDetector = useMemo( () => virtualChildren.reduce( (acc, child) => acc || child.forReanimated, @@ -43,7 +43,7 @@ export function InterceptingGestureDetector({ const NativeDetectorComponent = dispatchesAnimatedEvents ? AnimatedNativeDetector - : shouldUseReanimated + : shouldUseReanimatedDetector ? ReanimatedNativeDetector : HostGestureDetector; @@ -186,15 +186,15 @@ export function InterceptingGestureDetector({ )} // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerReanimatedStateChange={ - shouldUseReanimated ? reanimatedStateChangeHandler : undefined + shouldUseReanimatedDetector ? reanimatedStateChangeHandler : undefined } // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerReanimatedEvent={ - shouldUseReanimated ? reanimatedEventHandler : undefined + shouldUseReanimatedDetector ? reanimatedEventHandler : undefined } // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerReanimatedTouchEvent={ - shouldUseReanimated ? reanimatedTouchEventHandler : undefined + shouldUseReanimatedDetector ? reanimatedTouchEventHandler : undefined } handlerTags={handlerTags} style={nativeDetectorStyles.detector} From b970a2406427fba0a4f60278f9221b1dade053e9 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 18 Nov 2025 09:53:20 +0100 Subject: [PATCH 11/16] Rename context --- .../src/v3/detectors/GestureDetector.tsx | 4 ++-- .../InterceptingGestureDetector.tsx | 11 +++++++---- .../VirtualDetector/VirtualDetector.tsx | 8 ++++---- .../VirtualDetector/useDetectorContext.ts | 17 ----------------- .../useInterceptingDetectorContext.ts | 18 ++++++++++++++++++ 5 files changed, 31 insertions(+), 27 deletions(-) delete mode 100644 packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts create mode 100644 packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts diff --git a/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx index 0111b0d317..5807e49a6d 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx @@ -6,7 +6,7 @@ import { GestureDetectorProps as LegacyGestureDetectorProps, GestureDetector as LegacyGestureDetector, } from '../../handlers/gestures/GestureDetector'; -import { DetectorContext } from './VirtualDetector/useDetectorContext'; +import { InterceptingDetectorContext } from './VirtualDetector/useInterceptingDetectorContext'; import { VirtualDetector } from './VirtualDetector/VirtualDetector'; import { use } from 'react'; import { isTestEnv } from '../../utils'; @@ -31,7 +31,7 @@ export function GestureDetector( return ; } - const context = use(DetectorContext); + const context = use(InterceptingDetectorContext); return context ? ( )} 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 7c14165960..45bbf2b728 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 @@ -5,7 +5,10 @@ import { GestureHandlerEvent, DetectorCallbacks, } from '../../types'; -import { DetectorContext, DetectorContextValue } from './useDetectorContext'; +import { + InterceptingDetectorContext, + InterceptingDetectorContextValue, +} from './useInterceptingDetectorContext'; import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; import { configureRelations, ensureNativeDetectorComponent } from '../utils'; import { isComposedGesture } from '../../hooks/utils/relationUtils'; @@ -64,7 +67,7 @@ export function InterceptingGestureDetector({ setVirtualChildren((prev) => prev.filter((c) => c.viewTag !== childTag)); }, []); - const contextValue: DetectorContextValue = useMemo( + const contextValue: InterceptingDetectorContextValue = useMemo( () => ({ register, unregister, @@ -163,7 +166,7 @@ export function InterceptingGestureDetector({ }, [gesture]); return ( - + ({ moduleId={globalThis._RNGH_MODULE_ID}> {children} - + ); } diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index 53729c4318..65142766f9 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -1,15 +1,15 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { Wrap } from '../../../handlers/gestures/GestureDetector/Wrap'; import { findNodeHandle, Platform } from 'react-native'; -import { useDetectorContext } from './useDetectorContext'; +import { useInterceptingDetectorContext } from './useInterceptingDetectorContext'; import { isComposedGesture } from '../../hooks/utils/relationUtils'; import { NativeDetectorProps } from '../common'; import { configureRelations } from '../utils'; import { tagMessage } from '../../../utils'; import { DetectorCallbacks, VirtualChild } from '../../types'; -function useRequiredDetectorContext() { - const context = useDetectorContext(); +function useRequiredInterceptingDetectorContext() { + const context = useInterceptingDetectorContext(); if (!context) { throw new Error( tagMessage( @@ -27,7 +27,7 @@ export function VirtualDetector( // TODO: replace with MutationObserver when it rolls out in React Native 'use no memo'; - const { register, unregister } = useRequiredDetectorContext(); + const { register, unregister } = useRequiredInterceptingDetectorContext(); const viewRef = useRef(null); const [viewTag, setViewTag] = useState(-1); diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts deleted file mode 100644 index 90fff74d48..0000000000 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createContext, use } from 'react'; -import { VirtualChild } from '../../types'; - -export type DetectorContextValue = { - register: (child: VirtualChild) => void; - unregister: (child: number) => void; -}; - -export const DetectorContext = createContext(null); - -export function useDetectorContext() { - const ctx = use(DetectorContext); - if (!ctx) { - return null; - } - return ctx; -} diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts new file mode 100644 index 0000000000..97772cbf90 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts @@ -0,0 +1,18 @@ +import { createContext, use } from 'react'; +import { VirtualChild } from '../../types'; + +export type InterceptingDetectorContextValue = { + register: (child: VirtualChild) => void; + unregister: (child: number) => void; +}; + +export const InterceptingDetectorContext = + createContext(null); + +export function useInterceptingDetectorContext() { + const ctx = use(InterceptingDetectorContext); + if (!ctx) { + return null; + } + return ctx; +} From fcf488013d7fd3b9fc745fe435dc760c8c4f0324 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 18 Nov 2025 11:24:42 +0100 Subject: [PATCH 12/16] Fix crash on child change --- .../apple/RNGestureHandlerDetector.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm index b5e0059608..60078f6c62 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm @@ -211,7 +211,7 @@ - (void)attachHandlers:(const std::vector &)handlerTags } // This covers the case where `NativeViewGestureHandlers` are attached after child views were created. - if (self.subviews[0]) { + if (self.subviews.count != 0) { [self tryAttachNativeHandlersToChildView]; } } From 6d13417cc84255a2e795c7e753e0c63bbf2f545a Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 18 Nov 2025 11:28:17 +0100 Subject: [PATCH 13/16] Use single mode instead of aggregating bools --- .../InterceptingGestureDetector.tsx | 74 ++++++++++++++----- .../VirtualDetector/VirtualDetector.tsx | 22 ++++-- .../useInterceptingDetectorContext.ts | 8 ++ .../src/v3/types/DetectorTypes.ts | 2 - 4 files changed, 81 insertions(+), 25 deletions(-) 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 45bbf2b728..a2ed9680cf 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 @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import HostGestureDetector from '../HostGestureDetector'; import { VirtualChild, @@ -8,6 +8,7 @@ import { import { InterceptingDetectorContext, InterceptingDetectorContextValue, + InterceptingDetectorMode, } from './useInterceptingDetectorContext'; import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; import { configureRelations, ensureNativeDetectorComponent } from '../utils'; @@ -20,30 +21,38 @@ import { } from '../common'; import { tagMessage } from '../../../utils'; +interface SafeVirtualChildren { + viewTag: number; + handlerTags: number[]; + viewRef: unknown; +} + export function InterceptingGestureDetector({ gesture, children, }: InterceptingGestureDetectorProps) { const [virtualChildren, setVirtualChildren] = useState([]); - - const shouldUseReanimatedDetector = useMemo( + const safeVirtualChildren: SafeVirtualChildren[] = useMemo( () => - virtualChildren.reduce( - (acc, child) => acc || child.forReanimated, - gesture?.config.shouldUseReanimatedDetector ?? false - ), - [virtualChildren, gesture] + virtualChildren.map((child) => ({ + viewTag: child.viewTag, + handlerTags: child.handlerTags, + viewRef: child.viewRef, + })), + [virtualChildren] ); - - const dispatchesAnimatedEvents = useMemo( - () => - virtualChildren.reduce( - (acc, child) => acc || child.forAnimated, - gesture?.config.dispatchesAnimatedEvents ?? false - ), - [virtualChildren, gesture] + const [mode, setMode] = useState( + gesture?.config.shouldUseReanimatedDetector + ? InterceptingDetectorMode.REANIMATED + : gesture?.config.dispatchesAnimatedEvents + ? InterceptingDetectorMode.ANIMATED + : InterceptingDetectorMode.DEFAULT ); + const shouldUseReanimatedDetector = + mode === InterceptingDetectorMode.REANIMATED; + const dispatchesAnimatedEvents = mode === InterceptingDetectorMode.ANIMATED; + const NativeDetectorComponent = dispatchesAnimatedEvents ? AnimatedNativeDetector : shouldUseReanimatedDetector @@ -69,12 +78,41 @@ export function InterceptingGestureDetector({ const contextValue: InterceptingDetectorContextValue = useMemo( () => ({ + mode, + setMode: (newMode: InterceptingDetectorMode) => { + if ( + (newMode === InterceptingDetectorMode.REANIMATED && + mode === InterceptingDetectorMode.ANIMATED) || + (newMode === InterceptingDetectorMode.ANIMATED && + mode === InterceptingDetectorMode.REANIMATED) + ) { + throw new Error( + tagMessage( + 'InterceptingGestureDetector can only handle either Reanimated or Animated events at the same time.' + ) + ); + } + + setMode(newMode); + }, register, unregister, }), - [register, unregister] + [mode, register, unregister] ); + useEffect(() => { + if (gesture?.config?.dispatchesAnimatedEvents) { + contextValue.setMode(InterceptingDetectorMode.ANIMATED); + } else if (gesture?.config?.shouldUseReanimatedDetector) { + contextValue.setMode(InterceptingDetectorMode.REANIMATED); + } + }, [ + contextValue, + gesture?.config?.dispatchesAnimatedEvents, + gesture?.config?.shouldUseReanimatedDetector, + ]); + // It might happen only with ReanimatedNativeDetector if (!NativeDetectorComponent) { throw new Error( @@ -201,7 +239,7 @@ export function InterceptingGestureDetector({ } handlerTags={handlerTags} style={nativeDetectorStyles.detector} - virtualChildren={virtualChildren} + virtualChildren={safeVirtualChildren} moduleId={globalThis._RNGH_MODULE_ID}> {children} diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index 65142766f9..f7b0cd3a63 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -1,7 +1,10 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { Wrap } from '../../../handlers/gestures/GestureDetector/Wrap'; import { findNodeHandle, Platform } from 'react-native'; -import { useInterceptingDetectorContext } from './useInterceptingDetectorContext'; +import { + InterceptingDetectorMode, + useInterceptingDetectorContext, +} from './useInterceptingDetectorContext'; import { isComposedGesture } from '../../hooks/utils/relationUtils'; import { NativeDetectorProps } from '../common'; import { configureRelations } from '../utils'; @@ -27,7 +30,8 @@ export function VirtualDetector( // TODO: replace with MutationObserver when it rolls out in React Native 'use no memo'; - const { register, unregister } = useRequiredInterceptingDetectorContext(); + const { register, unregister, setMode } = + useRequiredInterceptingDetectorContext(); const viewRef = useRef(null); const [viewTag, setViewTag] = useState(-1); @@ -56,12 +60,20 @@ export function VirtualDetector( ? props.gesture.tags : [props.gesture.tag]; + if (props.gesture.config.dispatchesAnimatedEvents) { + throw new Error( + tagMessage( + 'VirtualGestureDetector cannot handle Animated events with native driver when used inside InterceptingGestureDetector. Use Reanimated or Animated events without native driver instead.' + ) + ); + } else if (props.gesture.config.shouldUseReanimatedDetector) { + setMode(InterceptingDetectorMode.REANIMATED); + } + const virtualChild: VirtualChild = { viewTag, handlerTags, methods: props.gesture.detectorCallbacks as DetectorCallbacks, - forReanimated: !!props.gesture.config.shouldUseReanimatedDetector, - forAnimated: !!props.gesture.config.dispatchesAnimatedEvents, // used by HostGestureDetector on web viewRef: Platform.OS === 'web' ? viewRef : undefined, }; @@ -71,7 +83,7 @@ export function VirtualDetector( return () => { unregister(viewTag); }; - }, [viewTag, props.gesture, register, unregister]); + }, [viewTag, props.gesture, register, unregister, setMode]); configureRelations(props.gesture); diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts index 97772cbf90..641f6ad05c 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts @@ -1,7 +1,15 @@ import { createContext, use } from 'react'; import { VirtualChild } from '../../types'; +export enum InterceptingDetectorMode { + DEFAULT, + ANIMATED, + REANIMATED, +} + export type InterceptingDetectorContextValue = { + mode: InterceptingDetectorMode; + setMode: (mode: InterceptingDetectorMode) => void; register: (child: VirtualChild) => void; unregister: (child: number) => void; }; 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 18ab3953f9..e7416b24ba 100644 --- a/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts @@ -25,8 +25,6 @@ export type VirtualChild = { viewTag: number; handlerTags: number[]; methods: DetectorCallbacks; - forReanimated: boolean; - forAnimated: boolean; // only set on web viewRef: unknown; From ddec85e7d53fb79747d470333e59d5dbf5aa0649 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 18 Nov 2025 14:32:18 +0100 Subject: [PATCH 14/16] Simplify context --- .../VirtualDetector/useInterceptingDetectorContext.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts index 641f6ad05c..11c607761e 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts @@ -18,9 +18,5 @@ export const InterceptingDetectorContext = createContext(null); export function useInterceptingDetectorContext() { - const ctx = use(InterceptingDetectorContext); - if (!ctx) { - return null; - } - return ctx; + return use(InterceptingDetectorContext); } From 3021f35988851438f6188f8b344b810ea64c8256 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 18 Nov 2025 14:32:36 +0100 Subject: [PATCH 15/16] Update error message --- .../detectors/VirtualDetector/InterceptingGestureDetector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a2ed9680cf..1c819abe43 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 @@ -88,7 +88,7 @@ export function InterceptingGestureDetector({ ) { throw new Error( tagMessage( - 'InterceptingGestureDetector can only handle either Reanimated or Animated events at the same time.' + 'InterceptingGestureDetector can only handle either Reanimated or Animated events.' ) ); } From 842521873101c33283f3034a95fd67fd2e293089 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 18 Nov 2025 14:39:14 +0100 Subject: [PATCH 16/16] Store virtual children in a set, rename variable --- .../InterceptingGestureDetector.tsx | 31 ++++++++++--------- .../VirtualDetector/VirtualDetector.tsx | 2 +- .../useInterceptingDetectorContext.ts | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) 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 1c819abe43..e8ae57a345 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 @@ -21,7 +21,7 @@ import { } from '../common'; import { tagMessage } from '../../../utils'; -interface SafeVirtualChildren { +interface VirtualChildrenForNative { viewTag: number; handlerTags: number[]; viewRef: unknown; @@ -31,10 +31,12 @@ export function InterceptingGestureDetector({ gesture, children, }: InterceptingGestureDetectorProps) { - const [virtualChildren, setVirtualChildren] = useState([]); - const safeVirtualChildren: SafeVirtualChildren[] = useMemo( + const [virtualChildren, setVirtualChildren] = useState>( + () => new Set() + ); + const virtualChildrenForNativeComponent: VirtualChildrenForNative[] = useMemo( () => - virtualChildren.map((child) => ({ + Array.from(virtualChildren).map((child) => ({ viewTag: child.viewTag, handlerTags: child.handlerTags, viewRef: child.viewRef, @@ -61,19 +63,18 @@ export function InterceptingGestureDetector({ const register = useCallback((child: VirtualChild) => { setVirtualChildren((prev) => { - const index = prev.findIndex((c) => c.viewTag === child.viewTag); - if (index !== -1) { - const updated = [...prev]; - updated[index] = child; - return updated; - } - - return [...prev, child]; + const newSet = new Set(prev); + newSet.add(child); + return newSet; }); }, []); - const unregister = useCallback((childTag: number) => { - setVirtualChildren((prev) => prev.filter((c) => c.viewTag !== childTag)); + const unregister = useCallback((child: VirtualChild) => { + setVirtualChildren((prev) => { + const newSet = new Set(prev); + newSet.delete(child); + return newSet; + }); }, []); const contextValue: InterceptingDetectorContextValue = useMemo( @@ -239,7 +240,7 @@ export function InterceptingGestureDetector({ } handlerTags={handlerTags} style={nativeDetectorStyles.detector} - virtualChildren={safeVirtualChildren} + virtualChildren={virtualChildrenForNativeComponent} moduleId={globalThis._RNGH_MODULE_ID}> {children} diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index f7b0cd3a63..706bf92fa6 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -81,7 +81,7 @@ export function VirtualDetector( register(virtualChild); return () => { - unregister(viewTag); + unregister(virtualChild); }; }, [viewTag, props.gesture, register, unregister, setMode]); diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts index 11c607761e..105f4aaa75 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts @@ -11,7 +11,7 @@ export type InterceptingDetectorContextValue = { mode: InterceptingDetectorMode; setMode: (mode: InterceptingDetectorMode) => void; register: (child: VirtualChild) => void; - unregister: (child: number) => void; + unregister: (child: VirtualChild) => void; }; export const InterceptingDetectorContext =