diff --git a/packages/docs-gesture-handler/docs/components/buttons.mdx b/packages/docs-gesture-handler/docs/components/buttons.mdx
index 34552ad0d6..854d65aa96 100644
--- a/packages/docs-gesture-handler/docs/components/buttons.mdx
+++ b/packages/docs-gesture-handler/docs/components/buttons.mdx
@@ -27,6 +27,8 @@ Gesture Handler library exposes components that render native touchable elements
On top of that all the buttons are wrapped with [`Native gesture`](/docs/gestures/use-native-gesture) and therefore allow for all its [properties](/docs/gestures/use-native-gesture#config) to be applied to them.
+Button callbacks always run on the JS thread. The Reanimated-specific configuration ([`disableReanimated`](/docs/fundamentals/reanimated-interactions#disablereanimated), [`runOnJS`](/docs/fundamentals/reanimated-interactions#runonjs)) is not available on buttons, and passing a worklet as a callback does not move it to the UI thread.
+
## RawButton
diff --git a/packages/docs-gesture-handler/docs/components/wrapped-components.mdx b/packages/docs-gesture-handler/docs/components/wrapped-components.mdx
index 960c349a9a..49d20415b5 100644
--- a/packages/docs-gesture-handler/docs/components/wrapped-components.mdx
+++ b/packages/docs-gesture-handler/docs/components/wrapped-components.mdx
@@ -7,6 +7,8 @@ sidebar_position: 5
Some components come with a [`Native`](/docs/gestures/use-native-gesture) gesture pre-applied. This allows them to participate in the gesture recognition process. Have a look at the example below.
+The callbacks of the pre-applied gesture always run on the JS thread. The Reanimated-specific configuration ([`disableReanimated`](/docs/fundamentals/reanimated-interactions#disablereanimated), [`runOnJS`](/docs/fundamentals/reanimated-interactions#runonjs)) is not available on these components.
+
```tsx
import { useState } from 'react';
import { Switch } from 'react-native';
diff --git a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
index 14e83c7750..508b2fc194 100644
--- a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
+++ b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
@@ -267,6 +267,40 @@ describe('[API v3] Components', () => {
expect(pressFn).toHaveBeenCalledTimes(1);
});
+ // BaseButton adds plain JS callbacks of its own, so a worklet passed by the
+ // user must not move the gesture to the Reanimated detector (#4628).
+ test('Rect Button with a worklet callback stays on the JS thread', () => {
+ const pressFn = jest.fn();
+ const onTouchesDown = Object.assign(jest.fn(), { __workletHash: 1 });
+
+ render(
+
+
+
+ );
+
+ const nativeGesture = getByGestureTestId('btn');
+
+ expect(nativeGesture.config.shouldUseReanimatedDetector).toBe(false);
+ expect(nativeGesture.config.disableReanimated).toBe(true);
+
+ act(() => {
+ fireGestureHandler(nativeGesture, [
+ { oldState: State.UNDETERMINED, state: State.BEGAN },
+ { oldState: State.BEGAN, state: State.ACTIVE },
+ { oldState: State.ACTIVE, state: State.END },
+ ]);
+ });
+
+ expect(pressFn).toHaveBeenCalledTimes(1);
+ });
+
describe('ScrollView', () => {
test('handles responder event passed through Pressable for keyboardShouldPersistTaps handled', async () => {
const { UNSAFE_getAllByType } = render(
diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx
index 75147f834d..60ba4641be 100644
--- a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx
+++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx
@@ -4,6 +4,7 @@ import { GestureDetectorType, InterceptingGestureDetector } from './detectors';
import { NativeDetector } from './detectors/NativeDetector';
import { VirtualDetector } from './detectors/VirtualDetector/VirtualDetector';
import { useNativeGesture } from './hooks/gestures';
+import type { NativeGestureConfig } from './hooks/gestures/native/NativeTypes';
import { NativeWrapperProps } from './hooks/utils';
import type {
NativeWrapperProperties,
@@ -36,7 +37,7 @@ export default function createNativeWrapper(
return res;
},
{
- gestureHandlerProps: { ...config }, // Watch out not to modify config
+ gestureHandlerProps: { ...config } as NativeGestureConfig, // Watch out not to modify config
childProps: {
enabled: props.enabled,
hitSlop: props.hitSlop,
@@ -45,9 +46,10 @@ export default function createNativeWrapper(
}
);
- if (gestureHandlerProps.disableReanimated === undefined) {
- gestureHandlerProps.disableReanimated = true;
- }
+ // Components built on top of the wrapper (e.g. BaseButton) add plain JS
+ // callbacks of their own, so the gesture must never use the Reanimated
+ // detector, whatever the caller passed.
+ gestureHandlerProps.disableReanimated = true;
const native = useNativeGesture(gestureHandlerProps);
diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
index 8ce7c62e56..ad3c07a131 100644
--- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
+++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
@@ -113,12 +113,15 @@ export const PropsWhiteLists = new Map<
export const EMPTY_WHITE_LIST = new Set();
+// `disableReanimated` and `runOnJS` are not wrapper props, but route them to
+// the gesture (where the wrapper overrides them) instead of the wrapped view.
export const NativeWrapperProps = new Set<
- keyof NativeWrapperProperties
+ keyof NativeWrapperProperties | 'disableReanimated' | 'runOnJS'
>([
...CommonConfig,
...HandlerCallbacks,
...NativeHandlerNativeProperties,
...ExternalRelationsConfig,
'disableReanimated',
+ 'runOnJS',
]);
diff --git a/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts
index db30a78c32..e87cd0b9a3 100644
--- a/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts
+++ b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts
@@ -16,7 +16,12 @@ export type WrapperSpecificProperties = {
) => void;
};
-export type NativeWrapperProperties = CommonGestureConfig &
+// Wrapped components always run their callbacks on the JS thread, so the
+// Reanimated-only config has no effect on them.
+export type NativeWrapperProperties = Omit<
+ CommonGestureConfig,
+ 'disableReanimated' | 'runOnJS'
+> &
GestureCallbacks &
NativeGestureNativeProperties &
ExternalRelations &