Skip to content

Conversation

m-bert
Copy link
Contributor

@m-bert m-bert commented Oct 13, 2025

Description

This PR adds runOnJS to gesture config.

Test plan

Tested on the following example:
import * as React from 'react';
import { Animated, Button, StyleSheet, View, Text } from 'react-native';
import {
  GestureHandlerRootView,
  NativeDetector,
  usePan,
  useSimultaneous,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';

const runtimeKind = (_worklet: boolean) => {
  'worklet';
  return _worklet ? 'worklet' : 'JS';
};

function SingleExampleWithState() {
  const [js, setJs] = React.useState(false);

  const gesture = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[SingleExampleWithState] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: js,
  });

  return (
    <View style={[styles.container, styles.center]}>
      <Button
        title="Change runtime"
        onPress={() => {
          setJs((v) => !v);
        }}
      />
      <NativeDetector gesture={gesture}>
        <Animated.View style={[styles.box]} />
      </NativeDetector>
    </View>
  );
}

function SingleExampleWithSharedValue() {
  const js = useSharedValue(false);

  const gesture = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[SingleExampleWithSharedValue] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: js,
  });

  return (
    <View style={[styles.container, styles.center]}>
      <Button
        title="Change runtime"
        onPress={() => {
          js.value = !js.value;
        }}
      />
      <NativeDetector gesture={gesture}>
        <Animated.View style={[styles.box]} />
      </NativeDetector>
    </View>
  );
}

function ComposedExample() {
  const [pan1JS, setPan1JS] = React.useState(false);
  const pan2JS = useSharedValue(false);

  const pan1 = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[ComposedExample | Pan1] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: pan1JS,
  });

  const pan2 = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[ComposedExample | Pan2] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: pan2JS,
  });

  const gesture = useSimultaneous(pan1, pan2);

  return (
    <View style={[styles.container, styles.center]}>
      <View style={[styles.center, { flexDirection: 'row', gap: 10 }]}>
        <Button
          title="Change Pan1 runtime"
          onPress={() => {
            setPan1JS((v) => !v);
          }}
        />

        <Button
          title="Change Pan2 runtime"
          onPress={() => {
            pan2JS.value = !pan2JS.value;
          }}
        />
      </View>

      <NativeDetector gesture={gesture}>
        <Animated.View style={[styles.box]} />
      </NativeDetector>
    </View>
  );
}

export default function App() {
  return (
    <GestureHandlerRootView style={[{ flex: 1 }, styles.center]}>
      <SingleExampleWithState />
      <SingleExampleWithSharedValue />
      <ComposedExample />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { gap: 10 },
  center: {
    display: 'flex',
    justifyContent: 'space-around',
    alignItems: 'center',
  },
  box: {
    width: 150,
    height: 150,
    borderRadius: 20,
    backgroundColor: 'pink',
  },
});

Copy link
Member

@MatiPl01 MatiPl01 left a comment

Choose a reason for hiding this comment

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

LGTM, left just 2 small questions

);
}

// This has to be done ASAP as other hooks depend `shouldUseReanimated`.
Copy link
Member

Choose a reason for hiding this comment

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

Is this comment still valid? After you removed the shouldUseReanimated prop?

Copy link
Member

Choose a reason for hiding this comment

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

I think it's still needed since shouldUseReanimated became shouldUseReanimatedDetector, but it definitely needs to be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is valid, changed to shouldUseReanimatedDetector in 7a4a014

config.needsPointerData = shouldHandleTouchEvents(config);
config.dispatchesAnimatedEvents = isAnimatedEvent(config.onUpdate);
config.dispatchesReanimatedEvents =
config.shouldUseReanimatedDetector && runOnJS !== true;
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it can be extracted to a helper function. The similar logic is already in the packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if that makes sense - one value comes from config, other from shared value listener. Extracting it into a separate function would make it more complicated (like using maybeUnpackValue inside), so I don't think it makes much sense.

Though I've unified checks in b470764 😅

);
}

// This has to be done ASAP as other hooks depend `shouldUseReanimated`.
Copy link
Member

Choose a reason for hiding this comment

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

I think it's still needed since shouldUseReanimated became shouldUseReanimatedDetector, but it definitely needs to be updated.

@m-bert m-bert merged commit a724f16 into next Oct 15, 2025
8 checks passed
@m-bert m-bert deleted the @mbert/run-on-js branch October 15, 2025 15:21
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