Skip to content

Commit 4e18d5f

Browse files
authored
Remove disableReanimated prop from components (#4650)
## Description A worklet callback passed to `RectButton` (or any `BaseButton`-based component) with `disableReanimated={false}` moved the gesture onto the Reanimated detector. `BaseButton` adds plain JS callbacks of its own (`onBegin`, `onActivate`, `onDeactivate`, `onFinalize`), which then ran on the UI runtime. In release this crashed on the first touch with "Tried to synchronously call a Remote Function", in dev Reanimated's `useHandler` threw at render. Components never supported worklet callbacks, so this makes that explicit: - `createNativeWrapper` always sets `disableReanimated` to `true`, whatever the caller passed. - `disableReanimated` and `runOnJS` are removed from the wrapper prop types. They are still routed to the gesture config so they do not leak onto the wrapped view. - Docs for buttons and wrapped components state that callbacks run on the JS thread. `Touchable` and `Pressable` are not affected: `Touchable` has no gesture config surface, `Pressable` hard-codes `disableReanimated` on its hooks. Closes #4628 ## Test plan - New test in api_v3.test.tsx: RectButton with a worklet onTouchesDown and disableReanimated={false} stays on the plain detector and still fires onPress. - Reporter's gist in the empty example screen on an Android emulator: all three buttons log on the JS thread, no crash.
1 parent 7b3c647 commit 4e18d5f

6 files changed

Lines changed: 54 additions & 6 deletions

File tree

‎packages/docs-gesture-handler/docs/components/buttons.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Gesture Handler library exposes components that render native touchable elements
2727

2828
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.
2929

30+
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.
31+
3032

3133
## RawButton
3234

‎packages/docs-gesture-handler/docs/components/wrapped-components.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ sidebar_position: 5
77

88
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.
99

10+
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.
11+
1012
```tsx
1113
import { useState } from 'react';
1214
import { Switch } from 'react-native';

‎packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,40 @@ describe('[API v3] Components', () => {
267267
expect(pressFn).toHaveBeenCalledTimes(1);
268268
});
269269

270+
// BaseButton adds plain JS callbacks of its own, so a worklet passed by the
271+
// user must not move the gesture to the Reanimated detector (#4628).
272+
test('Rect Button with a worklet callback stays on the JS thread', () => {
273+
const pressFn = jest.fn();
274+
const onTouchesDown = Object.assign(jest.fn(), { __workletHash: 1 });
275+
276+
render(
277+
<GestureHandlerRootView>
278+
<RectButton
279+
testID="btn"
280+
// @ts-expect-error Reanimated config is not part of the wrapper props.
281+
disableReanimated={false}
282+
onTouchesDown={onTouchesDown}
283+
onPress={pressFn}
284+
/>
285+
</GestureHandlerRootView>
286+
);
287+
288+
const nativeGesture = getByGestureTestId('btn');
289+
290+
expect(nativeGesture.config.shouldUseReanimatedDetector).toBe(false);
291+
expect(nativeGesture.config.disableReanimated).toBe(true);
292+
293+
act(() => {
294+
fireGestureHandler(nativeGesture, [
295+
{ oldState: State.UNDETERMINED, state: State.BEGAN },
296+
{ oldState: State.BEGAN, state: State.ACTIVE },
297+
{ oldState: State.ACTIVE, state: State.END },
298+
]);
299+
});
300+
301+
expect(pressFn).toHaveBeenCalledTimes(1);
302+
});
303+
270304
describe('ScrollView', () => {
271305
test('handles responder event passed through Pressable for keyboardShouldPersistTaps handled', async () => {
272306
const { UNSAFE_getAllByType } = render(

‎packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GestureDetectorType, InterceptingGestureDetector } from './detectors';
44
import { NativeDetector } from './detectors/NativeDetector';
55
import { VirtualDetector } from './detectors/VirtualDetector/VirtualDetector';
66
import { useNativeGesture } from './hooks/gestures';
7+
import type { NativeGestureConfig } from './hooks/gestures/native/NativeTypes';
78
import { NativeWrapperProps } from './hooks/utils';
89
import type {
910
NativeWrapperProperties,
@@ -36,7 +37,7 @@ export default function createNativeWrapper<TRef = unknown, TProps = unknown>(
3637
return res;
3738
},
3839
{
39-
gestureHandlerProps: { ...config }, // Watch out not to modify config
40+
gestureHandlerProps: { ...config } as NativeGestureConfig, // Watch out not to modify config
4041
childProps: {
4142
enabled: props.enabled,
4243
hitSlop: props.hitSlop,
@@ -45,9 +46,10 @@ export default function createNativeWrapper<TRef = unknown, TProps = unknown>(
4546
}
4647
);
4748

48-
if (gestureHandlerProps.disableReanimated === undefined) {
49-
gestureHandlerProps.disableReanimated = true;
50-
}
49+
// Components built on top of the wrapper (e.g. BaseButton) add plain JS
50+
// callbacks of their own, so the gesture must never use the Reanimated
51+
// detector, whatever the caller passed.
52+
gestureHandlerProps.disableReanimated = true;
5153

5254
const native = useNativeGesture(gestureHandlerProps);
5355

‎packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,15 @@ export const PropsWhiteLists = new Map<
113113

114114
export const EMPTY_WHITE_LIST = new Set<string>();
115115

116+
// `disableReanimated` and `runOnJS` are not wrapper props, but route them to
117+
// the gesture (where the wrapper overrides them) instead of the wrapped view.
116118
export const NativeWrapperProps = new Set<
117-
keyof NativeWrapperProperties<unknown>
119+
keyof NativeWrapperProperties<unknown> | 'disableReanimated' | 'runOnJS'
118120
>([
119121
...CommonConfig,
120122
...HandlerCallbacks,
121123
...NativeHandlerNativeProperties,
122124
...ExternalRelationsConfig,
123125
'disableReanimated',
126+
'runOnJS',
124127
]);

‎packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ export type WrapperSpecificProperties<T = unknown> = {
1616
) => void;
1717
};
1818

19-
export type NativeWrapperProperties<T = unknown> = CommonGestureConfig &
19+
// Wrapped components always run their callbacks on the JS thread, so the
20+
// Reanimated-only config has no effect on them.
21+
export type NativeWrapperProperties<T = unknown> = Omit<
22+
CommonGestureConfig,
23+
'disableReanimated' | 'runOnJS'
24+
> &
2025
GestureCallbacks<NativeHandlerData> &
2126
NativeGestureNativeProperties &
2227
ExternalRelations &

0 commit comments

Comments
 (0)