Skip to content

reanimatedEventHandler is passed to the plain host detector, so a non-function ends up on a DirectEventHandler prop #4428

Description

@antFrancon

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

  1. New Architecture app with Reanimated installed.
  2. Render a gesture with no worklet callbacks inside a GestureDetector, for example useNativeGesture().
  3. Check what reaches the host component. onGestureHandlerReanimatedEvent is 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Platform: AndroidThis issue is specific to AndroidPlatform: iOSThis issue is specific to iOSRepro providedA reproduction with a snack or repo is provided

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions