Skip to content

Conversation

@j-piasecki
Copy link
Member

Description

Adds an early return inside removeContextMenuListeners so that it doesn't throw when detach and dropHandler are called consecutively. This is possible in v3 api, where the gesture lifecycle isn't tied to the detector component.

Test plan

Found when working on #3813, not sure how to reproduce it outside of it, but to reproduce it there use this:

import * as React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import {
  GestureDetector,
  InterceptingGestureDetector,
  useTap,
} from 'react-native-gesture-handler';

export const COLORS = {
  NAVY: '#001A72',
  KINDA_RED: '#FFB2AD',
  YELLOW: '#FFF096',
  KINDA_GREEN: '#C4E7DB',
  KINDA_BLUE: '#A0D5EF',
};

function TextWithTap() {
  const tap = useTap({
    onStart: () => {
      'worklet';
      console.log('Tapped on text in its own component!');
    },
  });

  return (
    <GestureDetector gesture={tap}>
      <Text style={{ fontSize: 24, color: COLORS.KINDA_GREEN }}>
        This text is rendered in a separate component.
      </Text>
    </GestureDetector>
  );
}

function NativeDetectorExample() {
  const [entireVisible, setEntireVisible] = React.useState(true);
  const [firstVisible, setFirstVisible] = React.useState(true);
  const [secondVisible, setSecondVisible] = React.useState(true);
  const [thirdVisible, setThirdVisible] = React.useState(true);
  const [secondKey, setSecondKey] = React.useState(0);
  const [firstHasCallback, setFirstHasCallback] = React.useState(true);

  const tapAll = useTap({
    onStart: () => {
      'worklet';
      console.log('Tapped on text!');
    },
  });

  const tapFirstPart = useTap({
    onStart: firstHasCallback ? () => {
      'worklet';
      console.log('Tapped on first part!');
    }: () => {
      'worklet';
      console.log('First part tapped, but no callback set.');
    },
  });

  const tapSecondPart = useTap({
    onStart: () => {
      'worklet';
      console.log('Tapped on second part!');
    },
  });

  return (
    <View style={styles.subcontainer}>
      <Button title={(firstVisible ? 'Hide' : 'Show') + ' entire text'} onPress={() => setEntireVisible(v => !v)} />
      <Button title={(firstVisible ? 'Hide' : 'Show') + ' first part'} onPress={() => setFirstVisible(v => !v)} />
      <Button title={(secondVisible ? 'Hide' : 'Show') + ' second part'} onPress={() => setSecondVisible(v => !v)} />
      <Button title={(thirdVisible ? 'Hide' : 'Show') + ' third part'} onPress={() => setThirdVisible(v => !v)} />

      <Button title="Re-mount second part" onPress={() => setSecondKey(k => k + 1)} />
      <Button title={(firstHasCallback ? 'Disable' : 'Enable') + ' callback on first text'} onPress={() => setFirstHasCallback(v => !v)} />

      {entireVisible && <InterceptingGestureDetector gesture={tapAll}>
        <Text style={{ fontSize: 18, textAlign: 'center' }}>
          Some text example running with RNGH
          {firstVisible && <GestureDetector gesture={tapFirstPart}>
            <Text style={{ fontSize: 24, color: COLORS.NAVY }}>
              {' '}
              try tapping on this part
            </Text>
          </GestureDetector>}
          {secondVisible && <GestureDetector gesture={tapSecondPart}>
            <Text key={secondKey} style={{ fontSize: 28, color: COLORS.KINDA_BLUE }}>
              {' '}
              or on this part
            </Text>
          </GestureDetector>}
          {thirdVisible && <>
            {' '}
            <TextWithTap />
          </>}
          {' '}
          this part is not special :(
        </Text>
      </InterceptingGestureDetector>}
    </View>
  );
}

export default function NativeTextExample() {
  return (
    <View style={styles.container}>
      <NativeDetectorExample />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  subcontainer: {
    flex: 1,
    gap: 8,
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    fontSize: 18,
    textAlign: 'center',
    paddingHorizontal: 24,
  },
});

and press Hide entire text

@j-piasecki j-piasecki requested review from akwasniewski and m-bert and removed request for m-bert November 13, 2025 13:28
@j-piasecki j-piasecki merged commit 8d5eec6 into next Nov 13, 2025
1 check passed
@j-piasecki j-piasecki deleted the @jpiasecki/dont-clean-context-menu-uninitialized branch November 13, 2025 15:16
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.

3 participants