React Native Gesture Handler 3.0: Expo Migration Guide (2026)
Upgrade an Expo app from Gesture Handler 2 to 3 with the new hook API, Reanimated setup, renamed callbacks, relation hooks, and a small pan-card example you can actually use.
The short version
Gesture Handler 3 moves the primary API from builder chains like Gesture.Pan().onUpdate(...) to hooks like usePanGesture({ onUpdate }). For Expo apps, the upgrade is mostly dependency alignment, root setup, callback renames, and a careful pass over composed gestures.
If your Expo app already uses Gesture Handler 2 with Reanimated, the mental model stays familiar: gestures still drive shared values, and animated styles still render the movement. The difference is that gesture configuration now lives in hook options, which fits React components better and avoids a lot of manual memoization.
The official compatibility line is important: Gesture Handler 3 starts at React Native 0.82. Check your Expo SDK and React Native version before you touch app code, especially if you are upgrading an older production app.
1. Check the Expo app first
Start with versions, then install the package through Expo so you get a compatible native dependency set for your SDK.
npx expo install react-native-gesture-handler
npx expo install react-native-reanimated react-native-worklets
Reanimated plus react-native-worklets is the recommended path for gesture-driven interactions that need to run smoothly on the UI thread. You can still use React Native's Animated API for simpler cases, but most real gesture UI belongs with Reanimated.
If you are using Expo Go, check whether your SDK already includes the native versions you need. If you are using a development build, rebuild native code after changing native dependencies.
npx expo prebuild
npx expo run:ios
npx expo run:android
Version gate
Do not force Gesture Handler 3 into an app below React Native 0.82. Upgrade the Expo SDK first, then move the gestures.
2. Keep GestureHandlerRootView at the app root
Your gestures need to live under GestureHandlerRootView. Put it as close to the app root as possible so screen-level and component-level gestures can relate to each other correctly.
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack />
</GestureHandlerRootView>
);
}
In a classic Expo app without Expo Router, the same idea applies around your actual app component.
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ActualApp />
</GestureHandlerRootView>
);
}
On Android, content inside a native Modal may need its own GestureHandlerRootView. That is one of those boring setup details that saves you from mysterious "gesture works everywhere except this sheet" debugging later.
3. Move from builders to hooks
The big migration is mechanical, but it changes how the code reads. In Gesture Handler 2 you probably had builder chains.
import { Gesture } from "react-native-gesture-handler";
const pan = Gesture.Pan()
.onStart(() => {
startX.value = translateX.value;
})
.onUpdate((event) => {
translateX.value = startX.value + event.translationX;
})
.onEnd((event, success) => {
if (success) {
translateX.value = withSpring(0);
}
});
In Gesture Handler 3, the same pan becomes hook configuration. Callback names also shift: onStart becomes onActivate, onEnd becomes onDeactivate, and the old success boolean becomes event.canceled.
import { usePanGesture } from "react-native-gesture-handler";
const pan = usePanGesture({
onActivate: () => {
startX.value = translateX.value;
},
onUpdate: (event) => {
translateX.value = startX.value + event.translationX;
},
onDeactivate: (event) => {
if (!event.canceled) {
translateX.value = withSpring(0);
}
},
});
The shape is easier to scan once you get used to it. Gesture options, thresholds, and callbacks all sit in one object instead of being spread across chained method calls.
4. Build a small pan-card component
Here is a complete draggable card that uses Gesture Handler 3 hooks with Reanimated shared values. It is intentionally small so the migration pattern is obvious.
import { StyleSheet, Text } from "react-native";
import { GestureDetector, usePanGesture } from "react-native-gesture-handler";
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated";
export function PanCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const startX = useSharedValue(0);
const startY = useSharedValue(0);
const pan = usePanGesture({
onActivate: () => {
startX.value = translateX.value;
startY.value = translateY.value;
},
onUpdate: (event) => {
translateX.value = startX.value + event.translationX;
translateY.value = startY.value + event.translationY;
},
onDeactivate: (event) => {
if (!event.canceled) {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
}
},
});
const cardStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, cardStyle]}>
<Text style={styles.title}>Drag me</Text>
<Text style={styles.body}>Gesture Handler 3 hook API</Text>
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
card: {
width: 260,
minHeight: 150,
justifyContent: "center",
borderRadius: 18,
padding: 20,
backgroundColor: "#111827",
shadowColor: "#000",
shadowOpacity: 0.24,
shadowRadius: 18,
elevation: 8,
},
title: {
color: "#f8fafc",
fontSize: 24,
fontWeight: "800",
},
body: {
color: "#94a3b8",
marginTop: 8,
},
});
This is deliberately not a sortable list. For reorder screens, read the custom drag-and-drop guide separately. This article is about the Gesture Handler 3 migration surface: hooks, callbacks, relations, and setup.
5. Update callback and event assumptions
Most bugs in a Gesture Handler 3 migration come from old callback names or old event assumptions hiding in working code. Search the app for the old names and replace them intentionally.
onStartbecomesonActivate.onEndbecomesonDeactivate.onTouchesCancelledbecomesonTouchesCancel.- The old
successboolean becomesevent.canceled, with inverted meaning. onChangeis folded intoonUpdate; readchangeX,changeY, and related change values there.stateandoldStateare no longer on event objects, so use the lifecycle callbacks instead.
const tap = useTapGesture({
onDeactivate: (event) => {
if (event.canceled) {
return;
}
runOnJS(openDetails)();
},
});
That event.canceled rename is small, but it is the one I would double-check in code review. It is easy to preserve the old branch and accidentally reverse the behavior.
6. Migrate gesture relations
Composed gestures also move to hooks. A Gesture Handler 2 simultaneous gesture looked like this:
const pan = Gesture.Pan();
const pinch = Gesture.Pinch();
const gesture = Gesture.Simultaneous(pan, pinch);
In Gesture Handler 3, use the relation hook that describes the relationship.
import {
usePanGesture,
usePinchGesture,
useSimultaneousGestures,
} from "react-native-gesture-handler";
const pan = usePanGesture({});
const pinch = usePinchGesture({});
const gesture = useSimultaneousGestures(pan, pinch);
The key mappings are Gesture.Race() to useCompetingGestures(), Gesture.Simultaneous() to useSimultaneousGestures(), and Gesture.Exclusive() to useExclusiveGestures().
For cross-component relationships, config names also changed. Use simultaneousWith, requireToFail, and block instead of the old external-gesture builder methods.
7. Replace old touchable and button assumptions
Gesture Handler 3 introduces Touchable as the recommended replacement for older button and touchable APIs. Existing buttons may still work through deprecated or legacy exports, but new code should move toward Touchable.
- Replace
RectButtonstyle interactions withTouchableplus underlay or Android ripple props. - Replace
BorderlessButtonwithTouchableand a borderless Android ripple when you need that native feel. - Use legacy-prefixed components only when you need to preserve behavior while migrating a larger app in stages.
- Remember that
createNativeWrapperis deprecated and exported aslegacy_createNativeWrapper.
I would avoid converting every button and every gesture in the same commit. Upgrade the package, migrate the gesture hot paths, then clean up old touchables once screens are stable.
Expo migration gotchas
- Root view placement: if a gesture does not fire, confirm the component is under the same
GestureHandlerRootViewas the related gestures. - Android modals: wrap modal content in
GestureHandlerRootViewwhen gestures inside the modal are ignored. - Development builds: after changing native gesture dependencies, run
npx expo prebuildand rebuild the app. - Reanimated setup: keep the Reanimated Babel/plugin setup aligned with the version your Expo SDK expects.
- Old success checks: replace them with
!event.canceledand verify the branch still means "gesture completed". - Existing drag lists: do not rewrite complex reorder UI while also upgrading the library. Migrate the API first, then improve the interaction.
Final checklist
- Confirm the app is on React Native
0.82or newer through your Expo SDK. - Install Gesture Handler through
npx expo install react-native-gesture-handler. - Install and configure Reanimated plus
react-native-workletsfor UI-thread gestures. - Wrap the root app with
GestureHandlerRootView. - Move builder gestures to hook gestures like
usePanGesture. - Replace renamed callbacks and audit
event.canceledlogic. - Migrate composed gestures to relation hooks.
- Test iOS, Android, web if supported, and any modal or bottom-sheet screens.