Skip to content

[Android] Fix handlers cancelled while awaiting leaking in the orchestrator - #4402

Merged
m-bert merged 1 commit into
mainfrom
@mbert/fix-awaiting-handler-leak
Aug 7, 2026
Merged

[Android] Fix handlers cancelled while awaiting leaking in the orchestrator#4402
m-bert merged 1 commit into
mainfrom
@mbert/fix-awaiting-handler-leak

Conversation

@m-bert

@m-bert m-bert commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Description

On Android, cancelling a handler while it is awaiting another one (e.g. the single tap in Exclusive(doubleTap, singleTap) waiting for the double tap to fail) leaves it in the orchestrator forever. Both cleanup paths in cleanupFinishedHandlers skip handlers with isAwaiting set, and the rescue loop in onHandlerStateChange never reaches it because dropGestureHandler drops interaction relations on the JS thread before the posted cancel runs on the UI thread, so shouldHandlerWaitForOther no longer matches.

The leaked handler stays in gestureHandlers, which makes ButtonViewGroup.shouldBeginWithRecordedHandlers return false on every subsequent touch. As a result all button-based touchables (Pressable, RectButton, BaseButton, Touchables) stop responding app-wide until the app process is restarted. The most common trigger is unmounting a GestureDetector during the wait window.

This change clears isAwaiting when a handler reaches STATE_CANCELLED or STATE_FAILED, since such a handler can never be resolved by the one it was waiting for, letting the existing cleanup collect it. STATE_END stays pinned, as makeActive relies on it to send synthetic events. Going through onHandlerStateChange also covers cancel paths that never touch the registry, e.g. tryActivate cancelling an awaiting handler via shouldBeCancelledByFinishedHandler.

Fixes #4401

Test plan

Tested on the following code
import React, { useRef, useState } from 'react';
import {
  Pressable as RNPressable,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import {
  GestureDetector,
  Pressable,
  RectButton,
  useExclusiveGestures,
  useTapGesture,
} from 'react-native-gesture-handler';

// Repro for https://github.com/software-mansion/react-native-gesture-handler/issues/4401
// (Android): cancelling a handler while it is awaiting (Exclusive single tap
// waiting for double tap to fail) leaves it in the orchestrator forever.
//
// Steps:
// 1. Single-tap the purple box. 120ms later (inside the double-tap window,
//    while the single-tap handler is awaiting) the detector unmounts itself.
// 2. Try the probe buttons below. According to the issue, ALL RNGH-based
//    touchables should now be dead app-wide until app restart.

function ExclusiveBox({ onGone }: { onGone: () => void }) {
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

  const doubleTap = useTapGesture({
    runOnJS: true,
    numberOfTaps: 2,
    onActivate: () => console.log('[repro] double tap activated'),
  });

  const singleTap = useTapGesture({
    runOnJS: true,
    requireToFail: doubleTap,
    onActivate: () => console.log('[repro] single tap activated'),
    onTouchesUp: () => {
      // Unmount while the single-tap handler is awaiting double-tap failure
      if (timer.current == null) {
        timer.current = setTimeout(() => {
          console.log('[repro] unmounting detector while awaiting');
          onGone();
        }, 120);
      }
    },
  });

  const exclusive = useExclusiveGestures(doubleTap, singleTap);

  return (
    <GestureDetector gesture={exclusive}>
      <View style={[styles.box, { backgroundColor: 'rebeccapurple' }]}>
        <Text style={styles.boxLabel}>
          SINGLE-TAP ME{'\n'}(unmounts in 120ms)
        </Text>
      </View>
    </GestureDetector>
  );
}

export default function EmptyExample() {
  const [mounted, setMounted] = useState(true);
  const [detectorTaps, setDetectorTaps] = useState(0);
  const [pressableTaps, setPressableTaps] = useState(0);
  const [rectTaps, setRectTaps] = useState(0);
  const [rnTaps, setRnTaps] = useState(0);

  const probeTap = useTapGesture({
    runOnJS: true,
    onActivate: () => {
      console.log('[probe] GestureDetector tap');
      setDetectorTaps((n) => n + 1);
    },
  });

  return (
    <View style={styles.container}>
      {mounted ? (
        <ExclusiveBox onGone={() => setMounted(false)} />
      ) : (
        <RNPressable
          style={[styles.box, { backgroundColor: 'gray' }]}
          onPress={() => setMounted(true)}>
          <Text style={styles.boxLabel}>DETECTOR GONE — tap to remount</Text>
        </RNPressable>
      )}

      <GestureDetector gesture={probeTap}>
        <View style={[styles.probe, { backgroundColor: 'darkorange' }]}>
          <Text style={styles.boxLabel}>Probe detector: {detectorTaps}</Text>
        </View>
      </GestureDetector>

      <Pressable
        style={[styles.probe, { backgroundColor: 'seagreen' }]}
        onPress={() => {
          console.log('[probe] RNGH Pressable');
          setPressableTaps((n) => n + 1);
        }}>
        <Text style={styles.boxLabel}>RNGH Pressable: {pressableTaps}</Text>
      </Pressable>

      <RectButton
        style={[styles.probe, { backgroundColor: 'steelblue' }]}
        onPress={() => {
          console.log('[probe] RectButton');
          setRectTaps((n) => n + 1);
        }}>
        <Text style={styles.boxLabel}>RectButton: {rectTaps}</Text>
      </RectButton>

      <RNPressable
        style={[styles.probe, { backgroundColor: 'dimgray' }]}
        onPress={() => {
          console.log('[probe] RN core Pressable');
          setRnTaps((n) => n + 1);
        }}>
        <Text style={styles.boxLabel}>RN core Pressable: {rnTaps}</Text>
      </RNPressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    gap: 16,
    padding: 24,
  },
  box: {
    width: 260,
    height: 110,
    borderRadius: 12,
    justifyContent: 'center',
    alignItems: 'center',
  },
  probe: {
    width: 260,
    height: 56,
    borderRadius: 12,
    justifyContent: 'center',
    alignItems: 'center',
  },
  boxLabel: {
    color: 'white',
    fontWeight: 'bold',
    textAlign: 'center',
  },
});

Copilot AI review requested due to automatic review settings August 7, 2026 09:30
@coderabbitai

coderabbitai Bot commented Aug 7, 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: 119c9467-0e01-4223-965c-7ad5c7bfa707

📥 Commits

Reviewing files that changed from the base of the PR and between 73c51c7 and 63da7a7.

📒 Files selected for processing (1)
  • packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved gesture handling by properly clearing pending states when an awaiting gesture is cancelled or fails.
    • Prevents handlers from remaining stuck in an awaiting state after unsuccessful gesture recognition.

Walkthrough

The Android gesture orchestrator now clears isAwaiting when an awaiting handler reaches CANCELLED or FAILED, allowing existing finished-handler cleanup to process it.

Changes

Android awaiting handler cleanup

Layer / File(s) Summary
Clear awaiting state on terminal failure
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
Awaiting handlers clear isAwaiting before finished-state processing when they are cancelled or fail recognition.

Suggested reviewers: copilot, j-piasecki, coado

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the Android cleanup bug for handlers cancelled while awaiting.
Linked Issues check ✅ Passed The change clears isAwaiting for cancelled or failed handlers, enabling existing cleanup and preserving STATE_END behavior required by issue #4401.
Out of Scope Changes check ✅ Passed The six-line change is limited to the orchestrator cleanup behavior described in issue #4401.

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 an Android-side leak in GestureHandlerOrchestrator where a handler that gets cancelled/failed while isAwaiting could remain recorded indefinitely, eventually preventing RNGH-based touchables/gestures from beginning until app restart (as described in #4401).

Changes:

  • Clear handler.isAwaiting when the handler transitions to STATE_CANCELLED or STATE_FAILED inside onHandlerStateChange.
  • Allow the existing cleanupFinishedHandlers() logic to reset/remove these terminal-state handlers now that they’re no longer excluded by the !handler.isAwaiting guard.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@m-bert
m-bert requested a review from j-piasecki August 7, 2026 09:44
@m-bert
m-bert merged commit ce811da into main Aug 7, 2026
5 checks passed
@m-bert
m-bert deleted the @mbert/fix-awaiting-handler-leak branch August 7, 2026 10:14
RonenMars added a commit to RonenMars/threadbase-mobile that referenced this pull request Aug 26, 2026
…867)

Bumps
\[react-native-gesture-handler\](https://github.com/software-mansion/react-native-gesture-handler)
from 2.32.0 to 3.2.1. Release notes

_Sourced from [react-native-gesture-handler's
releases](https://github.com/software-mansion/react-native-gesture-handler/releases)._

> v3.2.1
> ------
> 
> 🐛 Bug fixes
> ------------
> 
> * Forward press handlers as `testOnly_*` in `PressableWithTouchable`
by [`@​huextrat`](https://github.com/huextrat) in
[software-mansion/react-native-gesture-handler#4416](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4416)
> 
> 🔢 Miscellaneous
> ----------------
> 
> * Update `Pressable` props by [`@​m-bert`](https://github.com/m-bert)
in
[software-mansion/react-native-gesture-handler#4421](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4421)
> 
> **Full Changelog**:
[https://github.com/software-mansion/react-native-gesture-handler/compare/v3.2.0...v3.2.1](https://github.com/software-mansion/react-native-gesture-handler/compare/v3.2.0...v3.2.1)
> 
> v3.2.0
> ------
> 
> ❗ Important changes
> -------------------
> 
> * feat: Adopt AGP v9 by [`@​hurali97`](https://github.com/hurali97) in
[software-mansion/react-native-gesture-handler#4263](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4263)
> * Implement `Pressable` based on `Touchable` by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4411](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4411)
> * \[Android\] Add hover callbacks to Touchable by
[`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4396](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4396)
> * \[iOS\] Add hover callbacks to Touchable by
[`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4397](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4397)
> * \[Web\] Add hover callbacks to Touchable by
[`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4398](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4398)
> * \[Web\] Refactor `Touchable` not to rely on `GestureDetector` by
[`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4344](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4344)
> * \[iOS\] Refactor `Touchable` not to rely on `GestureDetector` by
[`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4343](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4343)
> * \[Android\] Refactor `Touchable` not to rely on `GestureDetector` by
[`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4342](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4342)
> * Fix fatal crash `Cannot read property 'translationX' of undefined`
when a touch event is serialized without `allTouches` by
[`@​huextrat`](https://github.com/huextrat) in
[software-mansion/react-native-gesture-handler#4316](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4316)
> 
> 👍 Improvements
> ---------------
> 
> * \[Android\] Skip the underlay drawable when it can never be visible
by [`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4359](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4359)
> * \[Android\] Apply the button's managed handler config once per prop
transaction by [`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4357](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4357)
> * \[Android\] Configure the button's handler directly instead of
through a `ReadableMap` by
[`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4358](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4358)
> 
> 🐛 Bug fixes
> ------------
> 
> * \[Android\] Guard update events to only be dispatched in ACTIVE
state by [`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4332](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4332)
> * Pass empty callbacks to UI when `runOnJS` is `true` by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4326](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4326)
> * \[Web\] Fix incorrectly calculated `timeDelta` by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4329](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4329)
> * \[Web\] Fix incorrect `Tap` offset by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4330](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4330)
> * Fix `minVelocity` props behavior by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4327](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4327)
> * \[Android\] Fix `minDistance` being reset by partial config updates
by [`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4347](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4347)
> * Move Interceptor on `ScrollView`, not its content by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4331](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4331)
> * \[iOS\] Re-sync layer opacity and transform from retained props when
recycling buttons by [`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4360](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4360)
> * \[Android\] Properly handle `requestDisallowInterceptTouchEvent` for
v3 by [`@​j-piasecki`](https://github.com/j-piasecki) in
[software-mansion/react-native-gesture-handler#4367](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4367)
> * Fix `Touchable` not respecting `keyboardShouldPersistTaps="handled"`
by [`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4372](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4372)
> * \[macOS\] Fix touch events never being delivered by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4390](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4390)
> * fix: crash when mount listener fires after GestureDetector unmount
by [`@​kosmydel`](https://github.com/kosmydel) in
[software-mansion/react-native-gesture-handler#4268](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4268)
> * \[macOS\] Fix `Pan` activation criteria being ignored by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4387](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4387)
> * \[macOS\] Fix `manualActivation` never blocking gesture activation
by [`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4389](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4389)
> * \[iOS\] Fix touch events never being delivered to `VirtualDetector`
handlers by [`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4392](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4392)
> * \[iOS\] Fix gestures attached via `VirtualGestureDetector` never
recognizing continuous gestures by
[`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4393](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4393)
> * \[macOS\] Fix `Fling` not sending touch events and begin/end states
consistently by [`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4395](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4395)
> * \[Android\] Fix handlers cancelled while awaiting leaking in the
orchestrator by [`@​m-bert`](https://github.com/m-bert) in
[software-mansion/react-native-gesture-handler#4402](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4402)

... (truncated)

Commits

*
[`62f0f7d`](software-mansion/react-native-gesture-handler@62f0f7d)
Release v3.2.1
*
[`4716425`](software-mansion/react-native-gesture-handler@4716425)
Merge branch '3.2-stable' of
github.com:software-mansion/react-native-gesture...
*
[`f0ae48c`](software-mansion/react-native-gesture-handler@f0ae48c)
Update `Pressable` props
([#4421](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4421))
*
[`5f0f0d8`](software-mansion/react-native-gesture-handler@5f0f0d8)
Forward press handlers as `testOnly_*` in `PressableWithTouchable`
([#4416](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4416))
*
[`0a91db7`](software-mansion/react-native-gesture-handler@0a91db7)
Release v3.2.0
*
[`44046a6`](software-mansion/react-native-gesture-handler@44046a6)
\[Android\] Resolve the button event dispatcher by react tag
([#4415](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4415))
*
[`2469c1d`](software-mansion/react-native-gesture-handler@2469c1d)
Derive `Pressable` pressed state from `testOnly_pressed`
([#4414](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4414))
*
[`3f1bf74`](software-mansion/react-native-gesture-handler@3f1bf74)
Clear pending timers on unmount in StatefulPressable
([#4413](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4413))
*
[`50ae6a1`](software-mansion/react-native-gesture-handler@50ae6a1)
\[General\] Default GestureDetector moduleId to -1
([#4412](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4412))
*
[`8b661c9`](software-mansion/react-native-gesture-handler@8b661c9)
Implement `Pressable` based on `Touchable`
([#4411](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4411))
* Additional commits viewable in [compare
view](software-mansion/react-native-gesture-handler@v2.32.0...v3.2.1)
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.

Cancelled-while-awaiting handler is never cleaned up, permanently blocking all gestures (Android)

3 participants