Back to Tutorials
Expo Migration

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.

June 4, 2026 14 min read Paddy B
Expo React Native Gesture Handler 3.0 Reanimated Migration

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.

  • onStart becomes onActivate.
  • onEnd becomes onDeactivate.
  • onTouchesCancelled becomes onTouchesCancel.
  • The old success boolean becomes event.canceled, with inverted meaning.
  • onChange is folded into onUpdate; read changeX, changeY, and related change values there.
  • state and oldState are 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 RectButton style interactions with Touchable plus underlay or Android ripple props.
  • Replace BorderlessButton with Touchable and 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 createNativeWrapper is deprecated and exported as legacy_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 GestureHandlerRootView as the related gestures.
  • Android modals: wrap modal content in GestureHandlerRootView when gestures inside the modal are ignored.
  • Development builds: after changing native gesture dependencies, run npx expo prebuild and 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.canceled and 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.82 or newer through your Expo SDK.
  • Install Gesture Handler through npx expo install react-native-gesture-handler.
  • Install and configure Reanimated plus react-native-worklets for UI-thread gestures.
  • Wrap the root app with GestureHandlerRootView.
  • Move builder gestures to hook gestures like usePanGesture.
  • Replace renamed callbacks and audit event.canceled logic.
  • Migrate composed gestures to relation hooks.
  • Test iOS, Android, web if supported, and any modal or bottom-sheet screens.
Support tutorials