Skip to content

Conversation

@akwasniewski
Copy link
Contributor

@akwasniewski akwasniewski commented Oct 3, 2025

Description

To simplify migration the GestureDetector will check the type of the gesture it receives and render the detector accordingly it:

  • Checks if the gesture it received is V2 or V2:
  • If it's V2, it renders the old GestureDetector component
  • If it's V3, it checks for the GestureDetectorBoundary context:
  • If context is there, it's inside boundary and renders LogicDetector
  • If the context is not there, it renders NativeDetector

Why separate NativeDetector and GestureDetectorBoundary?

We did some performance tests on the NativeDetector after adding changes that handle attaching LogicDetector, which revealed that the new logic adds quite a bit of overhead, but only on the JS side. New logic on the native side does not seem to have a significant effect.
We concluded that the best solution is to create a separate component that will have all functionalities of NativeDetector and also allow LogicDetector attachment, while NativeDetector will be reverted to how it had been before implementing LogicDetector.

Test plan

Rendering LogicDetector:

import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { GestureDetectorBoundary, GestureDetector, useTap, GestureHandlerRootView } from 'react-native-gesture-handler';

import Svg, { Circle, Rect } from 'react-native-svg';

export default function SvgExample() {
  const circleElementTap = useTap({
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked circle')
    },
  });
  const rectElementTap = useTap({
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked parallelogram')
    },
  });

  return (
    <GestureHandlerRootView>
      <View style={styles.container}>
        <Text style={styles.header}>
          Overlapping SVGs with gesture detectors
        </Text>
        <View style={{ backgroundColor: 'tomato' }}>
          <GestureDetectorBoundary>
            <Svg
              height="250"
              width="250"
              onPress={() => console.log('SVG: clicked container')}>
              <GestureDetector gesture={circleElementTap}>
                <Circle
                  cx="125"
                  cy="125"
                  r="125"
                  fill="green"
                  onPress={() => console.log('SVG: clicked circle')}
                />
              </GestureDetector>
              <GestureDetector gesture={rectElementTap}>
                <Rect
                  skewX="45"
                  width="125"
                  height="250"
                  fill="yellow"
                  onPress={() => console.log('SVG: clicked parallelogram')}
                />
              </GestureDetector>
            </Svg>
          </GestureDetectorBoundary>
        </View>
        <Text>
          Tapping each color should read to a different console.log output
        </Text>
      </View>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 48,
  },
  header: {
    fontSize: 18,
    fontWeight: 'bold',
    margin: 10,
  },
});
Rendering NativeDetector:

import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  GestureDetector,
  useTap,
} from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  Easing,
  interpolateColor,
} from 'react-native-reanimated';

export default function SimpleTap() {
  const colorValue = useSharedValue(0);

  const tapGesture = useTap({
    onStart: () => {
      'worklet';
      colorValue.value = withTiming(colorValue.value === 0 ? 1 : 0, {
        duration: 400,
        easing: Easing.inOut(Easing.ease),
      });
    },
  });

  const animatedStyle = useAnimatedStyle(() => {
    const backgroundColor = interpolateColor(
      colorValue.value,
      [0, 1],
      ['#b58df1', '#ff7f50'] // purple → coral
    );

    return {
      backgroundColor,
    };
  });

  return (
    <View style={styles.centerView}>
      <GestureDetector gesture={tapGesture}>
        <Animated.View style={[styles.box, animatedStyle]} />
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  centerView: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    height: 120,
    width: 120,
    backgroundColor: '#b58df1',
    marginBottom: 30,
    borderRadius: 12,
  },
});
Rendering old v2 GestureDetector:

import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
} from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  Easing,
  interpolateColor,
} from 'react-native-reanimated';

export default function SimpleTap() {
  const colorValue = useSharedValue(0);

  const tapGesture = Gesture.Tap()
    .onStart(() => {
      'worklet';
      colorValue.value = withTiming(colorValue.value === 0 ? 1 : 0, {
        duration: 400,
        easing: Easing.inOut(Easing.ease),
      });
    });

  const animatedStyle = useAnimatedStyle(() => {
    const backgroundColor = interpolateColor(
      colorValue.value,
      [0, 1],
      ['#b58df1', '#ff7f50'] // purple → coral
    );

    return {
      backgroundColor,
    };
  });

  return (
    <View style={styles.centerView}>
      <GestureDetector gesture={tapGesture}>
        <Animated.View style={[styles.box, animatedStyle]} />
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  centerView: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    height: 120,
    width: 120,
    backgroundColor: '#b58df1',
    marginBottom: 30,
    borderRadius: 12,
  },
});

@akwasniewski akwasniewski marked this pull request as ready for review October 3, 2025 16:17
@akwasniewski
Copy link
Contributor Author

Currently the new Detector is named DelegateDetector as LogicDetector delegates their gestures to it. However, I'm not sure if it is a good name, I couldn't think of a better one. I'd appreciate any input on the naming.

Comment on lines 165 to 169
export type { NativeDetectorProps } from './v3/Detectors/common';
export { NativeDetector } from './v3/Detectors/NativeDetector';

export { LogicDetector } from './v3/Detectors/LogicDetector/LogicDetector';
export { DelegateDetector } from './v3/Detectors/LogicDetector/DelegateDetector';
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe let's also create index.ts in v3/detectors (and yes, I'd use lowercase for that directory 😅) which will export these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oki-dokie, I renamed directory in 88d4290 and created detectors/index in b7e9fcd

@@ -14,7 +14,9 @@ export const DetectorContext = createContext<DetectorContextType | null>(null);
export function useDetectorContext() {
const ctx = useContext(DetectorContext);
if (!ctx) {
throw new Error('Logic detector must be a descendant of a Native Detector');
throw new Error(
'Logic detector must be a descendant of a delegate detector'
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
'Logic detector must be a descendant of a delegate detector'
tagMessage('Logic detector must be a descendant of a delegate detector')

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tagMessage and moved the check into LogicDetector in 90cf5d5

@j-piasecki
Copy link
Member

j-piasecki commented Oct 13, 2025

What do you think about changing the naming (and logic) this way:

  • rename DelegateDetector to something along the lines of GestureDetectorBoundary
  • update GestureDetector so it:
    1. Checks if the gesture it received is V2 or V2:
    2. If it's V2, it renders whatever it's rendering currently
    3. If it's V3, it checks for the GestureDetectorBoundary context:
    • If context is there, it's inside boundary and renders LogicalDetector
    • If the context is not there, it renders NativeDetector

This way we have a GestureDetector component which works with both APIs (which was a plan since the beginning), and the only change needed to migrate is to update Gesture.* to use*, and add GestureDetectorBoundary around SVGs, etc.

@akwasniewski akwasniewski marked this pull request as draft October 15, 2025 07:33
@akwasniewski akwasniewski changed the title Separate logic native detector One detector to rule them all Oct 15, 2025
@akwasniewski akwasniewski marked this pull request as ready for review October 16, 2025 08:18
@akwasniewski akwasniewski requested a review from m-bert October 16, 2025 08:18
@akwasniewski
Copy link
Contributor Author

akwasniewski commented Oct 16, 2025

What do you think about changing the naming (and logic) this way:

* rename `DelegateDetector` to something along the lines of `GestureDetectorBoundary`

* update `GestureDetector` so it:
  
  1. Checks if the gesture it received is V2 or V2:
  2. If it's V2, it renders whatever it's rendering currently
  3. If it's V3, it checks for the `GestureDetectorBoundary` context:
  
  
  * If context is there, it's inside boundary and renders `LogicalDetector`
  * If the context is not there, it renders `NativeDetector`

This way we have a GestureDetector component which works with both APIs (which was a plan since the beginning), and the only change needed to migrate is to update Gesture.* to use*, and add GestureDetectorBoundary around SVGs, etc.

It seems that it is the right moment for that, so I implemented this functionality. Now we have one tag - GestureDetector, that renders either NativeDetector, LogicDetector or the old GestureDetector (v2) based on the gesture type and context.

@akwasniewski
Copy link
Contributor Author

I'm not sure about the name of the GestureDetectorBoundary, but couldn't figure out a better one so I settled on this one. Any ideas cc @m-bert? If that name stays I don't think it should accept any gestures as props, but ideally I would like to leave this functionality, while changing the name.

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