Repository navigation
reanimatedEventHandler is passed to the plain host detector, so a non-function ends up on a DirectEventHandler prop #4428
Description
Description
NativeDetector always passes reanimatedEventHandler to the native component, even when the gesture does not use the Reanimated detector:
// src/v3/detectors/NativeDetector.tsx
: {
onGestureHandlerReanimatedEvent:
gesture.detectorCallbacks.reanimatedEventHandler,
};The issue is that two different conditions decide whether the handler exists and whether the Reanimated detector is used.
useGestureCallbacks builds the handler whenever disableReanimated is not set:
// src/v3/hooks/useGestureCallbacks.ts:53
let reanimatedEventHandler;
if (!config.disableReanimated) {shouldUseReanimatedDetector also requires worklet callbacks:
// src/v3/hooks/utils/configUtils.ts:80
config.shouldUseReanimatedDetector =
!config.disableReanimated &&
Reanimated !== undefined &&
hasWorkletEventHandlers(config) &&
!config.dispatchesAnimatedEvents;So a gesture with no worklet callbacks gets shouldUseReanimatedDetector === false and renders the plain HostGestureDetector, which forwards props as they are. But reanimatedEventHandler is still defined, so the host component receives Reanimated's { workletEventHandler } object instead of a function.
onGestureHandlerReanimatedEvent is a codegen DirectEventHandler. React checks that the listener is a function in getListener, so when something dispatches that event through one of these detectors you get:
Expected `onGestureHandlerReanimatedEvent` listener to be a function, instead got a value of `object` type.
Most of the time it stays hidden. A handler on a plain detector gets dispatchesReanimatedEvents: shouldUseReanimatedDetector && !runOnJS, which is false, so it never emits the event. It only becomes visible when a handler ends up attached to a detector that is not its own. That is how we found it, and it threw on every gesture frame.
Composed gestures are affected the same way, since useComposedGesture calls useComposedEventHandler unconditionally. That includes RNGH's own Pressable and Touchable.
We saw it on iOS, but the branch above is the shared non-web path, so Android runs the same code.
Suggested fix
Only pass the handler when the Reanimated detector is actually used:
: {
onGestureHandlerReanimatedEvent: gesture.config
.shouldUseReanimatedDetector
? gesture.detectorCallbacks.reanimatedEventHandler
: undefined,
};The prop is part of the static view config, so undefined only means there is no JS listener. Native still emits the event.
Happy to open a PR if you want.
Steps to reproduce
- New Architecture app with Reanimated installed.
- Render a gesture with no worklet callbacks inside a
GestureDetector, for exampleuseNativeGesture(). - Check what reaches the host component.
onGestureHandlerReanimatedEventis an object, not a function.
The repro app shows four gestures side by side and prints, for each one, which detector NativeDetector renders and the typeof of the handler it gets. The first three are wrong, the last one is a worklet gesture as a control.
A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
https://github.com/antFrancon/rngh-reanimated-handler-repro
Gesture Handler version
3.2.1 (also unchanged on main)
React Native version
0.86.2
Platforms
iOS, Android
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
iOS simulator
Device model
iPhone 17 (iOS 26.5)
Acknowledgements
Yes
Activity
m-bert commentedon Aug 17, 2026
Hi @antFrancon! Thank you for submitting this issue, and a follow-up fix ❤️
This seems like a bigger issue, I'll investigate that!
antFrancon commentedon Aug 17, 2026
Thanks! Dumping what we found here in case it saves you time, we spent a while on this one. Caveat first: we never got a standalone repro for it, so treat it as a lead and not a proper report.
Here's what we saw on iOS, using a local dev warning that names both sides of the binding:
21 was the pan gesture of one component instance, 43 a
useNativeGesture()from a different one. So a handler bound to a detector owned by another instance, not a parent/child mixup.The static side is the bit we're confident about. In
apple/RNGestureHandlerDetector.mm,cancelAllObservationsForOwner:only ever gets called fromwillMoveToWindow:when the new window is nil:UIKit doesn't call
willMoveToWindow:if the window is already nil, so a detector unmounted from a subtree that's already off window never runs it, and hits the recycle pool with its observations still live.Then
prepareForRecyclecallssetDefaultProps, which reallocates_subscribedHandlers. That set is the only thingattachHandlers:buildshandlersToDetachfrom, so once a view has been through a recycle there's nothing left that could cancel those observations. A surviving one can still fireattachReadyHandler:, which binds toself, and re-parent a foreign handler onto whatever that view has become.Android doesn't have the hole,
detachAllHandlers()runs from bothonDetachedFromWindow()andonDropViewInstance.It might be the iOS half of #3965 as well. Same
<Activity>symptom, fixed by #3967, but that one only touched Android and the reporter mentioned they had no iOS device to test on.We patch it locally by hoisting the teardown into
detachAllHandlersand calling it fromprepareForRecycletoo, beforesetDefaultPropswipes_moduleIdand the handler sets it needs. It has to guard on_moduleId == -1and a null manager instead ofreact_native_assert, since it can now run on a view that never got props. One gotcha there:handlerManagerForModuleId:isreturn _managers[moduleId];over a staticstd::unordered_map, so calling it with -1 inserts a null entry.Can send it as a PR if that's useful, but you'll know the recycling paths better than me.
m-bert commentedon Aug 17, 2026
Thanks for a quick response @antFrancon, I've already looked at it and seems like we have the same conclusion. I've already opened #4440. Thank you for your investigation ❤️
Also, one note to the repro you've posted:
in that case both callbacks will be treated as worklets because
onActivatewill be workletized automaticallyPass `reanimatedEventHandler` only when the Reanimated detector is us…
[iOS] Detach handlers when the detector view is recycled (#4440)