Skip to content

Commit 1cf805a

Browse files
authored
[General] Allow explicit opt-in for the Animated codepaths when not using native driver (#3824)
## Description Currently, we're enabling the Animated codepath only when the handler passed to `onUpdate` is an `Animated.Event` with `useNativeDriver`. When `useNativeDriver` is not enabled, `Animated.event` returns a function, and we have no way of differentiating between it and a function passed by the user. This is not ideal since the animated codepath receives events in a different format (wrapped with `nativeEvent`). This meant that the code would stop working when switching between using the native driver and not. The same would happen when trying to use a native driver in code supposed to also run on web, where it's not available - the gesture would work on mobile, but crash on web. To avoid this, this PR adds an option to explicitly opt-in to the animated codepath by setting `useAnimated` in the gesture config. This will automatically disable Reanimated and throw an error when user if the user tries to reenable it. ## Test plan Tested on the `Native Detector` example in the basic app, as well as on the expo app on the same example for web.
1 parent 8b43cf5 commit 1cf805a

11 files changed

Lines changed: 110 additions & 29 deletions

File tree

packages/react-native-gesture-handler/src/v3/detectors/NativeDetector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function NativeDetector<THandlerData, TConfig>({
5151
onGestureHandlerReanimatedTouchEvent={
5252
gesture.detectorCallbacks.onReanimatedTouchEvent
5353
}
54+
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
5455
onGestureHandlerAnimatedEvent={
5556
gesture.detectorCallbacks.onGestureHandlerAnimatedEvent
5657
}

packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,15 @@ export function InterceptingGestureDetector<THandlerData, TConfig>({
126126
const createGestureEventHandler = useCallback(
127127
(key: keyof DetectorCallbacks<THandlerData>) => {
128128
return (e: GestureHandlerEvent<THandlerData>) => {
129-
if (gesture?.detectorCallbacks[key]) {
129+
if (typeof gesture?.detectorCallbacks[key] === 'function') {
130+
// @ts-expect-error passing event to a union of functions where only one is typed as such
130131
gesture.detectorCallbacks[key](e);
131132
}
132133

133134
virtualChildren.forEach((child) => {
134135
const method = child.methods[key];
135-
if (method) {
136+
if (typeof method === 'function') {
137+
// @ts-expect-error passing event to a union of functions where only one is typed as such
136138
method(e);
137139
}
138140
});

packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { prepareUpdateHandlers, isAnimatedEvent } from '../../utils';
1+
import { prepareUpdateHandlers } from '../../utils';
22
import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper';
33
import { getUpdateHandler } from '../updateHandler';
44
import { BaseGestureConfig } from '../../../types';
@@ -20,13 +20,18 @@ export function useGestureUpdateEvent<THandlerData, TConfig>(
2020
lastUpdateEvent: undefined,
2121
};
2222

23-
return isAnimatedEvent(config.onUpdate)
23+
return config.dispatchesAnimatedEvents
2424
? undefined
2525
: getUpdateHandler(
2626
handlerTag,
2727
handlers,
2828
jsContext,
2929
changeEventCalculator
3030
);
31-
}, [handlerTag, config.onUpdate, config.changeEventCalculator]);
31+
}, [
32+
handlerTag,
33+
config.onUpdate,
34+
config.dispatchesAnimatedEvents,
35+
config.changeEventCalculator,
36+
]);
3237
}

packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@ export function useGesture<THandlerData, TConfig>(
3030
// This has to be done ASAP as other hooks depend `shouldUseReanimatedDetector`.
3131
prepareConfig(config);
3232

33-
if (config.dispatchesAnimatedEvents && config.shouldUseReanimatedDetector) {
34-
throw new Error(
35-
tagMessage(
36-
`${type}: You cannot use Animated.Event together with callbacks running on the UI thread. Either remove Animated.Event from onUpdate, or set runOnJS property to true on the gesture.`
37-
)
38-
);
39-
}
40-
4133
// TODO: Call only necessary hooks depending on which callbacks are defined (?)
4234
const {
4335
onGestureHandlerStateChange,

packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
11
import { useGestureStateChangeEvent } from './callbacks/js/useGestureStateChangeEvent';
22
import { useGestureUpdateEvent } from './callbacks/js/useGestureUpdateEvent';
33
import { useGestureTouchEvent } from './callbacks/js/useGestureTouchEvent';
4-
import { AnimatedEvent, BaseGestureConfig } from '../types';
5-
import { checkMappingForChangeProperties, isAnimatedEvent } from './utils';
4+
import { AnimatedEvent, BaseGestureConfig, GestureUpdateEvent } from '../types';
5+
import {
6+
checkMappingForChangeProperties,
7+
isNativeAnimatedEvent,
8+
} from './utils';
69
import { useReanimatedStateChangeEvent } from './callbacks/reanimated/useReanimatedStateChangeEvent';
710
import { useReanimatedUpdateEvent } from './callbacks/reanimated/useReanimatedUpdateEvent';
811
import { useReanimatedTouchEvent } from './callbacks/reanimated/useReanimatedTouchEvent';
12+
import { tagMessage } from '../../utils';
13+
14+
function guardJSAnimatedEvent(handler: (...args: unknown[]) => void) {
15+
return (...args: unknown[]) => {
16+
try {
17+
handler(...args);
18+
} catch (e) {
19+
if (
20+
e instanceof Error &&
21+
e.message.includes('Bad event of type undefined for key')
22+
) {
23+
throw new Error(
24+
tagMessage(
25+
'The event mapping inside an Animated.event is invalid. ' +
26+
'Please make sure you are using the correct structure for the gesture event:\n\n' +
27+
'{ nativeEvent: { handlerData: { /* your mappings here */ } } }'
28+
)
29+
);
30+
}
31+
32+
throw e;
33+
}
34+
};
35+
}
936

1037
export function useGestureCallbacks<THandlerData, TConfig>(
1138
handlerTag: number,
@@ -31,10 +58,21 @@ export function useGestureCallbacks<THandlerData, TConfig>(
3158
onReanimatedTouchEvent = useReanimatedTouchEvent(handlerTag, config);
3259
}
3360

34-
let onGestureHandlerAnimatedEvent: AnimatedEvent | undefined;
35-
if (isAnimatedEvent(config.onUpdate)) {
36-
checkMappingForChangeProperties(config.onUpdate);
37-
onGestureHandlerAnimatedEvent = config.onUpdate;
61+
let onGestureHandlerAnimatedEvent:
62+
| ((event: GestureUpdateEvent<THandlerData>) => void)
63+
| AnimatedEvent
64+
| undefined;
65+
if (config.dispatchesAnimatedEvents) {
66+
if (__DEV__ && isNativeAnimatedEvent(config.onUpdate)) {
67+
checkMappingForChangeProperties(config.onUpdate);
68+
}
69+
70+
if (__DEV__ && !isNativeAnimatedEvent(config.onUpdate)) {
71+
// @ts-expect-error At this point we know it's not a native animated event, so it's callable
72+
onGestureHandlerAnimatedEvent = guardJSAnimatedEvent(config.onUpdate);
73+
} else {
74+
onGestureHandlerAnimatedEvent = config.onUpdate;
75+
}
3876
}
3977

4078
return {

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
SingleGestureName,
77
} from '../../types';
88
import { hasWorkletEventHandlers, maybeUnpackValue } from './reanimatedUtils';
9-
import { isAnimatedEvent, shouldHandleTouchEvents } from './eventUtils';
9+
import { isNativeAnimatedEvent, shouldHandleTouchEvents } from './eventUtils';
1010
import {
1111
allowedNativeProps,
1212
EMPTY_WHITE_LIST,
@@ -20,12 +20,46 @@ export function prepareConfig<THandlerData, TConfig extends object>(
2020
) {
2121
const runOnJS = maybeUnpackValue(config.runOnJS);
2222

23+
if (
24+
__DEV__ &&
25+
isNativeAnimatedEvent(config.onUpdate) &&
26+
!config.useAnimated
27+
) {
28+
console.warn(
29+
tagMessage(
30+
'You are using Animated.event in onUpdate without setting useAnimated to true. ' +
31+
'This may lead to unexpected behavior. If you intend to use Animated.event, ' +
32+
'please set useAnimated to true in the gesture config.'
33+
)
34+
);
35+
}
36+
37+
config.dispatchesAnimatedEvents =
38+
config.useAnimated || isNativeAnimatedEvent(config.onUpdate);
39+
40+
// Validate that the user is not trying to mix Animated and Reanimated before updating the config.
41+
if (
42+
__DEV__ &&
43+
config.dispatchesAnimatedEvents &&
44+
(config.disableReanimated === false || config.runOnJS === false)
45+
) {
46+
throw new Error(
47+
tagMessage(
48+
'Animated cannot be used together with Reanimated in the same gesture. Please choose either Animated or Reanimated for handling gesture events.'
49+
)
50+
);
51+
}
52+
53+
if (config.dispatchesAnimatedEvents) {
54+
config.disableReanimated = true;
55+
}
56+
2357
config.shouldUseReanimatedDetector =
2458
!config.disableReanimated &&
2559
Reanimated !== undefined &&
26-
hasWorkletEventHandlers(config);
60+
hasWorkletEventHandlers(config) &&
61+
!config.dispatchesAnimatedEvents;
2762
config.needsPointerData = shouldHandleTouchEvents(config);
28-
config.dispatchesAnimatedEvents = isAnimatedEvent(config.onUpdate);
2963
config.dispatchesReanimatedEvents =
3064
config.shouldUseReanimatedDetector && !runOnJS;
3165
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function isEventForHandlerWithTag<THandlerData>(
4040
return event.handlerTag === handlerTag;
4141
}
4242

43-
export function isAnimatedEvent<THandlerData>(
43+
export function isNativeAnimatedEvent<THandlerData>(
4444
callback:
4545
| ((event: GestureUpdateEvent<THandlerData>) => void)
4646
| AnimatedEvent

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const PropsToFilter = new Set<BaseGestureConfig<unknown, unknown>>([
5353
'changeEventCalculator',
5454
'disableReanimated',
5555
'shouldUseReanimatedDetector',
56+
'useAnimated',
57+
'runOnJS',
5658

5759
// Relations
5860
'simultaneousWithExternalGesture',

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export type GestureCallbacks<THandlerData> = {
2525
event: GestureStateChangeEvent<THandlerData>,
2626
didSucceed: boolean
2727
) => void;
28-
onUpdate?: (event: GestureUpdateEvent<THandlerData>) => void | AnimatedEvent;
28+
onUpdate?:
29+
| ((event: GestureUpdateEvent<THandlerData>) => void)
30+
| AnimatedEvent;
2931
onTouchesDown?: (event: GestureTouchEvent) => void;
3032
onTouchesMove?: (event: GestureTouchEvent) => void;
3133
onTouchesUp?: (event: GestureTouchEvent) => void;
@@ -48,6 +50,7 @@ export type InternalConfigProps<THandlerData> = {
4850

4951
export type CommonGestureConfig = {
5052
disableReanimated?: boolean;
53+
useAnimated?: boolean;
5154
} & WithSharedValue<
5255
{
5356
runOnJS?: boolean;

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
StateChangeEvent,
44
UpdateEvent,
55
TouchEvent,
6+
GestureUpdateEvent,
67
} from './EventTypes';
78

89
export type DetectorCallbacks<THandlerData> = {
@@ -18,7 +19,10 @@ export type DetectorCallbacks<THandlerData> = {
1819
| undefined
1920
| ((event: UpdateEvent<THandlerData>) => void);
2021
onReanimatedTouchEvent: undefined | ((event: TouchEvent) => void);
21-
onGestureHandlerAnimatedEvent: undefined | AnimatedEvent;
22+
onGestureHandlerAnimatedEvent:
23+
| undefined
24+
| AnimatedEvent
25+
| ((event: GestureUpdateEvent<THandlerData>) => void);
2226
};
2327

2428
export type VirtualChild = {

0 commit comments

Comments
 (0)