Skip to content

Pass reanimatedEventHandler only when the Reanimated detector is used - #4429

Open
antFrancon wants to merge 2 commits into
software-mansion:mainfrom
antFrancon:fix/reanimated-handler-on-plain-detector
Open

Pass reanimatedEventHandler only when the Reanimated detector is used#4429
antFrancon wants to merge 2 commits into
software-mansion:mainfrom
antFrancon:fix/reanimated-handler-on-plain-detector

Conversation

@antFrancon

Copy link
Copy Markdown

Description

NativeDetector always passes reanimatedEventHandler to the native component, even when the gesture does not use the Reanimated detector. Two different conditions decide whether the handler exists and whether the Reanimated detector is used.

useGestureCallbacks builds the handler whenever disableReanimated is unset:

// src/v3/hooks/useGestureCallbacks.ts
let reanimatedEventHandler;

if (!config.disableReanimated) {

shouldUseReanimatedDetector additionally requires worklet callbacks:

// src/v3/hooks/utils/configUtils.ts
config.shouldUseReanimatedDetector =
  !config.disableReanimated &&
  Reanimated !== undefined &&
  hasWorkletEventHandlers(config) &&
  !config.dispatchesAnimatedEvents;

So a gesture with no worklet callbacks gets shouldUseReanimatedDetector === false, renders the plain HostGestureDetector, which forwards props verbatim, and receives Reanimated's { workletEventHandler } object under onGestureHandlerReanimatedEvent. That prop is a codegen DirectEventHandler, so React throws out of getListener instead of dispatching:

Expected `onGestureHandlerReanimatedEvent` listener to be a function, instead got a value of `object` type.

It stays latent most of the time, because a handler on a plain detector gets dispatchesReanimatedEvents: shouldUseReanimatedDetector && !runOnJS, which is false, so it never emits the event. It surfaces once a handler ends up attached to a detector that is not its own. The prop is invalid either way.

This passes the handler only when the Reanimated detector is actually used. The event prop is part of the static view config, so undefined only means there is no JS listener, and native still emits the event.

I left the web branch untouched, since I have not tested whether the same mismatch applies there. Happy to extend it if you think it does.

Fixes #4428

Test plan

  • yarn ts-check, yarn lint:js and yarn test in packages/react-native-gesture-handler.
  • Repro app: https://github.com/antFrancon/rngh-reanimated-handler-repro. It renders four gestures and prints, for each, which detector NativeDetector picks and the typeof of the handler it receives.
    • Before: useNativeGesture(), usePanGesture() and useTapGesture({ onActivate }) all render HostGestureDetector with typeof reanimatedEventHandler === 'object'.
    • After: those three receive undefined, and the worklet control still gets the handler on ReanimatedNativeDetector.
  • We have been running this change as a patch in an app on Expo SDK 57, RN 0.86.2, React 19.2.3, Reanimated 4.5.3, Fabric, Hermes, iOS, with no regressions in gesture behaviour.

The handler is built whenever `disableReanimated` is unset, but
`shouldUseReanimatedDetector` additionally requires worklet callbacks.
A gesture with none renders the plain host component, which forwards
props verbatim, so it received Reanimated's event handler object under
`onGestureHandlerReanimatedEvent`, a codegen DirectEventHandler prop.
React then throws out of `getListener` instead of dispatching.
Copilot AI lite review requested due to automatic review settings August 15, 2026 18:30
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a3249a52-919a-4a71-89a3-d0a9a1b1b72a

📥 Commits

Reviewing files that changed from the base of the PR and between 1e24ffa and d849577.

📒 Files selected for processing (1)
  • packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx

Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved native gesture handling when Reanimated detection is disabled.
    • Prevented invalid event handlers from being passed during gesture processing.

Walkthrough

Changes

Native detector event handling

Layer / File(s) Summary
Conditional Reanimated handler assignment
packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx
onGestureHandlerReanimatedEvent now receives the Reanimated handler only when shouldUseReanimatedDetector is enabled. Otherwise, NativeDetector passes undefined.

Merge Risk: ⚪ Minimal · up to d8495

The change limits the Reanimated event handler to the detector that uses it, preventing an invalid handler object from being passed to plain gesture detectors. No actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: conditional passing of reanimatedEventHandler based on detector usage.
Linked Issues check ✅ Passed The change addresses issue #4428 by passing reanimatedEventHandler only when the Reanimated detector is selected, including native and web paths.
Out of Scope Changes check ✅ Passed The changes are limited to the relevant NativeDetector handler assignment and align with issue #4428 and the stated PR objectives.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a New Architecture crash where NativeDetector could forward Reanimated’s { workletEventHandler } object into a codegen DirectEventHandler prop (onGestureHandlerReanimatedEvent) when the Reanimated detector is not in use, causing React to throw when resolving listeners.

Changes:

  • Gate onGestureHandlerReanimatedEvent so it’s only provided when gesture.config.shouldUseReanimatedDetector is true (native path), otherwise pass undefined to avoid an invalid listener type.
  • Add an in-file comment documenting why the guard is necessary and how the mismatch occurs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@m-bert m-bert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @antFrancon! Thank you for submitting this PR! Could you please also add the same guard for the web part? I know that it should be safe since we have forReanimated, but I'd like to keep these in sync 😅

Comment on lines +65 to +68
// `reanimatedEventHandler` is built whenever `disableReanimated` is unset, but
// `shouldUseReanimatedDetector` additionally requires worklet callbacks. When it is
// false we render the plain host component, which forwards props verbatim, so passing
// the handler would put a non-function on a codegen `DirectEventHandler` prop.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this

Suggested change
// `reanimatedEventHandler` is built whenever `disableReanimated` is unset, but
// `shouldUseReanimatedDetector` additionally requires worklet callbacks. When it is
// false we render the plain host component, which forwards props verbatim, so passing
// the handler would put a non-function on a codegen `DirectEventHandler` prop.

Web is safe today because the handler only emits Reanimated events when
`forReanimated` is set, which itself comes from
`dispatchesReanimatedEvents` and so already requires
`shouldUseReanimatedDetector`. Hoisting the guard keeps both branches in
sync rather than relying on that.
@antFrancon

Copy link
Copy Markdown
Author

Done, pushed.

I hoisted the guard into a single reanimatedEventHandler const that both branches use, rather than repeating the ternary four times. Net effect is a bit less code than before.

And you are right that web is already safe: forReanimated is set from config.dispatchesReanimatedEvents, which is shouldUseReanimatedDetector && !runOnJS, so the handler never emits there. The guard just makes it explicit instead of load-bearing on that chain.

ts-check, lint:js and test all pass in packages/react-native-gesture-handler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants