Commit bbb49c3
authored
## Description
Currently we call
[`useReanimatedEventHandler`](https://github.com/software-mansion/react-native-gesture-handler/blob/f922b3b23c23ae9e5d77e6d716f7d6f8c02fe9b5/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts#L57)
with original callbacks from config. However, objects captured in the
callbacks' closures will be frozen by Reanimated as they will be passed
to UI, even if `runOnJS` is set to `true`. This triggers warning from
worklets, as well as prevents modification of objects in closure.
```
[Worklets] Tried to modify key `current` of an object which has been already passed to a worklet.
```
To fix that, we pass empty callbacks when `runOnJS` is `true`.
## Test plan
<details>
<summary>Tested on the following code:</summary>
```tsx
import React, { useCallback, useMemo, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';
import { scheduleOnRN } from 'react-native-worklets';
// Empirical test matrix for the `runOnJS` noop-substitution fix.
//
// Pad A - static `runOnJS: true`. Callbacks mutate a plain `session` object;
// before the fix dev builds froze it ("0 updates in ~1.7T ms" + warnings).
// Expected: real numbers, no `Tried to modify key` warnings.
//
// Pad B - `runOnJS` driven by React state, toggled by the button below it.
// Callbacks report which runtime executed them. Expected: "JS runtime"
// while ON, then after toggling OFF the real callbacks must be re-registered
// on the UI runtime - a drag must report "UI runtime". If the lazy swap
// were broken, the UI path would fire the registered noops and the status
// would never update.
//
// Pad C - `runOnJS` as a SharedValue, flipped by writing `sv.value` directly
// (no re-render). Expected: "JS runtime" while true, "UI runtime" after the
// flip - proving the SharedValue path still eagerly registers the real
// callbacks and toggles without React involvement.
function runtimeName() {
'worklet';
return (globalThis as { _WORKLET?: boolean })._WORKLET
? 'UI runtime'
: 'JS runtime';
}
export default function EmptyExample() {
// --- Pad A: static runOnJS: true, plain mutable object ---
const [summaryA, setSummaryA] = useState('drag to measure');
const session = useMemo(() => ({ startTime: 0, updateCount: 0 }), []);
const panA = usePanGesture({
onActivate: () => {
session.startTime = Date.now();
session.updateCount = 0;
},
onUpdate: () => {
session.updateCount += 1;
},
onDeactivate: () => {
const duration = Date.now() - session.startTime;
setSummaryA(`${session.updateCount} updates in ${duration} ms`);
},
runOnJS: true,
});
// --- Pad B: runOnJS toggled through React state ---
const [jsModeB, setJsModeB] = useState(true);
const [statusB, setStatusB] = useState('drag to check');
const reportB = useCallback((runtime: string) => {
setStatusB(`activated on ${runtime}`);
}, []);
const panB = usePanGesture({
onActivate: () => {
scheduleOnRN(reportB, runtimeName());
},
runOnJS: jsModeB,
});
// --- Pad C: runOnJS as a SharedValue, flipped without a re-render ---
const jsModeC = useSharedValue(true);
const [statusC, setStatusC] = useState('drag to check');
const reportC = useCallback((runtime: string) => {
setStatusC(`activated on ${runtime}`);
}, []);
const panC = usePanGesture({
onActivate: () => {
scheduleOnRN(reportC, runtimeName());
},
runOnJS: jsModeC,
});
return (
<View style={styles.container}>
<GestureDetector gesture={panA}>
<View style={styles.pad}>
<Text style={styles.padLabel}>Pad A - static true</Text>
</View>
</GestureDetector>
<Text style={styles.status}>{summaryA}</Text>
<GestureDetector gesture={panB}>
<View style={styles.pad}>
<Text style={styles.padLabel}>Pad B - state toggle</Text>
</View>
</GestureDetector>
<Text style={styles.status}>{statusB}</Text>
<Pressable
style={styles.button}
onPress={() => setJsModeB((prev) => !prev)}>
<Text style={styles.buttonLabel}>
{`B: runOnJS is ${jsModeB ? 'ON' : 'OFF'} - toggle`}
</Text>
</Pressable>
<GestureDetector gesture={panC}>
<View style={styles.pad}>
<Text style={styles.padLabel}>Pad C - SharedValue toggle</Text>
</View>
</GestureDetector>
<Text style={styles.status}>{statusC}</Text>
<Pressable
style={styles.button}
onPress={() => {
jsModeC.value = !jsModeC.value;
}}>
<Text style={styles.buttonLabel}>
C: flip SharedValue (no rerender)
</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
pad: {
alignSelf: 'stretch',
height: 110,
borderRadius: 12,
backgroundColor: '#eef',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#99a',
justifyContent: 'center',
alignItems: 'center',
},
padLabel: {
color: '#667',
fontSize: 15,
},
status: {
marginTop: 6,
marginBottom: 12,
textAlign: 'center',
color: '#666',
},
button: {
alignSelf: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: '#dde',
marginBottom: 16,
},
buttonLabel: {
color: '#334',
},
});
```
</details>
1 parent ed6e159 commit bbb49c3
2 files changed
Lines changed: 137 additions & 2 deletions
File tree
- packages/react-native-gesture-handler/src
- __tests__
- v3/hooks
Lines changed: 123 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
Lines changed: 14 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
| |||
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
54 | 66 | | |
55 | | - | |
| 67 | + | |
56 | 68 | | |
57 | 69 | | |
58 | 70 | | |
59 | | - | |
| 71 | + | |
60 | 72 | | |
61 | 73 | | |
62 | 74 | | |
| |||
0 commit comments