Mastering React Native Gesture Handler 3: The Complete Guide to the New Hooks API, Relations, and Worklet Workflows
React Native Gesture Handler 3 introduces a major modernization of mobile gesture handling by transitioning from the legacy builder pattern (Gesture.Pan()) to a fully hook-based API (usePanGesture(), useTapGesture(), usePinchGesture()). Designed specifically for React Native's New Architecture (Fabric), Gesture Handler 3 streamlines callback lifecycles, integrates seamlessly with React Compiler patterns, and introduces direct SharedValue configuration binding.
Executive Summary (TL;DR)
- Hook-Based Declarations: Builder chains (
Gesture.Pan().onUpdate(...)) are replaced with dedicated hooks (usePanGesture({ onUpdate: ... })), improving alignment with standard React idioms. - Modernized Callbacks:
onStartis renamed toonActivate,onEndis renamed toonDeactivate, and the old(event, success)parameter signature is replaced with a singleevent.canceledproperty. - Streamlined Delta Tracking: The separate
onChangecallback has been merged directly intoonUpdate, which now exposes properties likechangeXandchangeY. - Declarative Relations: Gesture arbitration is now configured via properties like
simultaneousWith,requireToFail, andblockdirectly in the hook config, or composed using helper hooks likeuseSimultaneousGestures. - Dynamic SharedValues: Gesture configuration options (e.g.,
enabled, activation thresholds) can consume ReanimatedSharedValueinstances directly without triggering component re-renders.
The Paradigm Shift: From Builder Chains to Idiomatic Hooks
Gesture Handler 3 replaces the chainable Gesture.* builder pattern with dedicated React hooks such as usePanGesture, useTapGesture, and usePinchGesture. Instead of mutating and chaining configuration methods on an object instance, developers now pass a structured configuration object directly into the hook. This architectural pivot simplifies component logic, improves TypeScript inference, and natively supports modern React paradigms and compiler workflows.
In Gesture Handler 2, gestures were declared using chainable builder objects:
// Legacy RNGH v2 Builder Pattern (Deprecated)
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
const panGesture = Gesture.Pan()
.minDistance(10)
.onStart(() => {
'worklet';
console.log('Started');
})
.onUpdate((event) => {
'worklet';
translateX.value = event.translationX;
})
.onEnd((event, success) => {
'worklet';
if (success) {
console.log('Completed');
}
});In React Native Gesture Handler Documentation v3, every gesture type is instantiated via a top-level hook. The configuration object accepts options and lifecycle callbacks cleanly:
// Modern RNGH v3 Hook Pattern
import { usePanGesture, GestureDetector } from 'react-native-gesture-handler';
const panGesture = usePanGesture({
minDistance: 10,
onActivate: () => {
'worklet';
console.log('Activated');
},
onUpdate: (event) => {
'worklet';
translateX.value = event.translationX;
},
onDeactivate: (event) => {
'worklet';
if (!event.canceled) {
console.log('Completed successfully');
}
},
});Available Gesture Hooks in RNGH 3
The new API provides dedicated hooks for every standard gesture type:
usePanGesture(config): Continuous panning, dragging, and velocity tracking.useTapGesture(config): Discrete single or multi-tap recognition.usePinchGesture(config): Multi-touch pinch-to-zoom scaling.useRotationGesture(config): Two-finger rotation tracking.useLongPressGesture(config): Timed touch-and-hold interactions.useFlingGesture(config): Directional swipe gestures with distance thresholds.useNativeGesture(config): Interoperability with native platform scroll containers.
Callback Modernization: Renamed Lifecycles and Event Properties
Gesture Handler 3 standardizes callback naming to more accurately reflect native gesture state transitions. Callbacks like onStart and onEnd have been updated to onActivate and onDeactivate, while onChange is unified into onUpdate. In addition, gesture outcome checking now uses an explicit canceled boolean property on the event object rather than a separate parameter.
[ Touch Down ]
│
▼
┌───────────────┐
│ onBegin │
└───────┬───────┘
│
(Threshold Passed)
│
▼
┌───────────────┐
│ onActivate │ (Formerly onStart)
└───────┬───────┘
│
(Continuous Moves)
│
▼
┌───────────────┐
│ onUpdate │ (Now includes delta changeX / changeY)
└───────┬───────┘
│
(Touch Lifted / Cancelled)
│
▼
┌───────────────┐
│ onDeactivate │ (Formerly onEnd — checks event.canceled)
└───────┬───────┘
│
▼
┌───────────────┐
│ onFinalize │ (Always runs for terminal cleanup)
└───────────────┘Key Lifecycle Changes
| Legacy Callback (v2) | Modern Callback (v3) | Behavioral Details |
|---|---|---|
onStart |
onActivate |
Fires precisely when the gesture transitions from BEGAN to ACTIVE. |
onEnd |
onDeactivate |
Fires when the gesture transitions out of ACTIVE. Receives event containing event.canceled. |
onChange |
Merged into onUpdate |
onUpdate now directly provides delta fields (changeX, changeY, scaleChange, etc.). |
onTouchesCancelled |
onTouchesCancel |
Standardized naming for direct multi-touch cancellation events. |
onFinalize |
onFinalize |
Unchanged; guaranteed to fire on every terminal transition (END, FAILED, CANCELLED). |
Handling Success vs. Cancellation
In v2, onEnd received (event, success). In v3, onDeactivate receives the gesture event payload containing event.canceled:
const pan = usePanGesture({
onDeactivate: (event) => {
'worklet';
if (event.canceled) {
// The gesture was interrupted by the OS or a competing gesture
console.log('Gesture was cancelled');
return;
}
// Clean completion
console.log('Gesture completed with final velocity:', event.velocityX);
},
});Dynamic Configuration and Direct SharedValue Binding
Gesture Handler 3 introduces first-class integration with Reanimated SharedValues directly inside hook configuration parameters. Developers can bind dynamic values—such as toggling enabled or updating activation thresholds—without causing React re-render cycles. This ensures gesture configurations stay responsive while keeping the component tree completely static.
import React from 'react';
import { usePanGesture } from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';
export const DynamicDraggable = () => {
const isDragAllowed = useSharedValue(true);
const minPanDistance = useSharedValue(20);
// SharedValues can be passed directly into configuration props
const panGesture = usePanGesture({
enabled: isDragAllowed,
minDistance: minPanDistance,
onUpdate: (e) => {
'worklet';
// Handle drag updates
},
});
return (
// Component markup
null
);
};Disabling Reanimated on Specific Gestures
If you are building a lightweight gesture that does not require Reanimated worklet bridging (for example, simple analytics tracking on the JavaScript thread), you can pass disableReanimated: true in the configuration:
const jsTapGesture = useTapGesture({
disableReanimated: true,
onActivate: () => {
// Runs directly on the JS thread without worklet wrapping
trackAnalyticsEvent('button_tapped');
},
});Gesture Relations: Declarative Coordination and Conflict Resolution
Coordinating competing or simultaneous gestures in RNGH 3 is accomplished by passing referenced gesture instances into relationship properties within the hook configuration. Options such as simultaneousWith, requireToFail, and block allow gestures to declare dependencies explicitly. For grouping simultaneous gestures inside a single detector, the useSimultaneousGestures hook provides a clean declarative wrapper.
usePanGesture & usePinchGesture
│
┌───────────────┴───────────────┐
▼ ▼
[ Pan Gesture Hook ] [ Pinch Gesture Hook ]
(simultaneousWith: [pinch]) (simultaneousWith: [pan])
│ │
└───────────────┬───────────────┘
│
▼
useSimultaneousGestures(pan, pinch)
│
▼
<GestureDetector gesture={...}>The Three Core Relation Properties
simultaneousWith: Allows multiple gestures to activate concurrently without cancelling one another.const pan = usePanGesture({ simultaneousWith: [pinchGesture], });requireToFail: Delays gesture activation until the specified gesture explicitly fails. Commonly used to distinguish between single-tap and double-tap interactions.const singleTap = useTapGesture({ requireToFail: [doubleTapGesture], onActivate: () => { 'worklet'; console.log('Single tap confirmed'); }, });block: Explicitly blocks target gestures from activating while the declaring gesture is recognized.const drawerPan = usePanGesture({ block: [nativeScrollGesture], });
Composing Gestures with useSimultaneousGestures
When attaching multiple cooperating gestures to the same view, combine them using useSimultaneousGestures:
import {
usePanGesture,
usePinchGesture,
useSimultaneousGestures,
GestureDetector,
} from 'react-native-gesture-handler';
const pan = usePanGesture({ /* ... */ });
const pinch = usePinchGesture({ /* ... */ });
// Combine both gestures into a single detector handle
const composedGesture = useSimultaneousGestures(pan, pinch);
return (
<GestureDetector gesture={composedGesture}>
<Animated.View style={animatedStyle} />
</GestureDetector>
);Complete Implementation: Interactive Multi-Touch Canvas
Combining multiple gesture hooks enables sophisticated multi-touch canvas manipulation with smooth spring physics and momentum. In this production example, usePanGesture, usePinchGesture, and useTapGesture are coordinated using gesture relations and Reanimated shared values. The canvas supports concurrent panning and zooming while cleanly handling double-tap resets and cancellation lifecycles.
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
GestureDetector,
GestureHandlerRootView,
usePanGesture,
usePinchGesture,
useTapGesture,
useSimultaneousGestures,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated';
const SPRING_CONFIG = {
damping: 15,
stiffness: 120,
mass: 0.8,
};
export const InteractiveCanvas: React.FC = () => {
const translationX = useSharedValue(0);
const translationY = useSharedValue(0);
const prevTranslationX = useSharedValue(0);
const prevTranslationY = useSharedValue(0);
const scale = useSharedValue(1);
const prevScale = useSharedValue(1);
const isInteracting = useSharedValue(false);
// 1. Pan Gesture: Dragging and momentum decay
const panGesture = usePanGesture({
onBegin: () => {
'worklet';
isInteracting.value = true;
prevTranslationX.value = translationX.value;
prevTranslationY.value = translationY.value;
},
onUpdate: (event) => {
'worklet';
translationX.value = prevTranslationX.value + event.translationX;
translationY.value = prevTranslationY.value + event.translationY;
},
onDeactivate: (event) => {
'worklet';
if (!event.canceled) {
translationX.value = withDecay({
velocity: event.velocityX,
clamp: [-400, 400],
});
translationY.value = withDecay({
velocity: event.velocityY,
clamp: [-600, 600],
});
}
},
onFinalize: () => {
'worklet';
isInteracting.value = false;
},
});
// 2. Pinch Gesture: Focal scaling
const pinchGesture = usePinchGesture({
simultaneousWith: [panGesture],
onBegin: () => {
'worklet';
isInteracting.value = true;
prevScale.value = scale.value;
},
onUpdate: (event) => {
'worklet';
const nextScale = prevScale.value * event.scale;
scale.value = Math.min(Math.max(nextScale, 0.5), 4.0);
},
onDeactivate: (event) => {
'worklet';
if (!event.canceled && scale.value < 1) {
scale.value = withSpring(1, SPRING_CONFIG);
}
},
onFinalize: () => {
'worklet';
isInteracting.value = false;
},
});
// 3. Tap Gesture: Double-tap to reset transformation
const doubleTapGesture = useTapGesture({
numberOfTaps: 2,
maxDelayMs: 250,
onActivate: () => {
'worklet';
translationX.value = withSpring(0, SPRING_CONFIG);
translationY.value = withSpring(0, SPRING_CONFIG);
scale.value = withSpring(1, SPRING_CONFIG);
},
});
// 4. Compose Pan and Pinch
const panAndPinch = useSimultaneousGestures(panGesture, pinchGesture);
// 5. Animated Style
const animatedStyle = useAnimatedStyle(() => {
'worklet';
return {
transform: [
{ translateX: translationX.value },
{ translateY: translationY.value },
{ scale: scale.value },
],
opacity: isInteracting.value ? 0.9 : 1.0,
};
});
return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.canvasBounds}>
<GestureDetector gesture={doubleTapGesture}>
<GestureDetector gesture={panAndPinch}>
<Animated.View style={[styles.targetCard, animatedStyle]} />
</GestureDetector>
</GestureDetector>
</View>
</GestureHandlerRootView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090d16',
},
canvasBounds: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
targetCard: {
width: 240,
height: 240,
borderRadius: 24,
backgroundColor: '#0284c7',
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 16,
},
});Migration Guide: Upgrading from Gesture Handler 2 to 3
Upgrading an existing application to Gesture Handler 3 requires replacing builder chains with their respective hook counterparts and updating callback identifiers. Teams must ensure their application runs on React Native 0.82 or newer with the New Architecture enabled, as legacy bridge support is removed. Following a structured migration checklist ensures seamless adoption without breaking gesture arbitration.
Migration Checklist
- Verify Minimum Requirements: Ensure your project is on React Native 0.82+ with the New Architecture (Fabric) enabled. Check the React Native Gesture Handler GitHub Repository for release notes.
- Convert Builders to Hooks: Replace calls to
Gesture.Pan(),Gesture.Tap(), andGesture.Pinch()withusePanGesture({}),useTapGesture({}), andusePinchGesture({}). - Rename Lifecycle Callbacks:
- Replace
.onStart(...)withonActivate: (...) => {}. - Replace
.onEnd(...)withonDeactivate: (event) => {}. - Replace
.onTouchesCancelled(...)withonTouchesCancel: (...) => {}.
- Replace
- Update Cancellation Checks: Change
onEnd((event, success) => { if (success) ... })toonDeactivate((event) => { if (!event.canceled) ... }). - Migrate
onChange: Move delta calculation logic from.onChange()intoonUpdate, readingevent.changeX,event.changeY, orevent.scaleChange. - Update Composition: Replace
Gesture.Simultaneous(g1, g2)withuseSimultaneousGestures(g1, g2)or configuresimultaneousWith: [g2]inside the hook config. - Adopt the New
<Touchable>: Migrate legacyRectButton,BorderlessButton, orTouchableOpacitywrappers to RNGH 3's unified<Touchable>component. Refer to the Software Mansion Reanimated Documentation for compatible animation patterns.
Frequently Asked Questions
Can I mix the legacy Gesture builder API with the new hook-based API in the same component?
No, mixing the legacy Gesture.* builder API with the new use*Gesture hook API within the same interaction hierarchy is not supported. Gesture relationships such as simultaneousWith, requireToFail, and block require all participating gestures to use the same internal representation. When migrating a screen or component, convert all associated gestures to the new hook API.
How does event cancellation work in RNGH 3 compared to RNGH 2?
In Gesture Handler 2, onEnd received a second argument boolean named success (e.g., (event, success) => {}). In Gesture Handler 3, onDeactivate receives a single event object containing a canceled boolean property. A value of event.canceled === true indicates that the gesture was interrupted, cancelled by the operating system, or superseded by a competing recognizer.
Why did Gesture Handler 3 remove the onChange callback in favor of onUpdate?
In earlier versions, onChange was provided as a convenience wrapper around onUpdate to calculate change deltas (the difference in touch position or scale between the current and previous frame). In Gesture Handler 3, these delta calculations (changeX, changeY, scaleChange, etc.) are computed natively and attached directly to the onUpdate event object, removing the need for a redundant callback.
About the Author
Furkan Çetinkaya is a Mobile-focused Software Developer specializing in React Native, native bridge integrations (Kotlin & Swift), and supporting backend services. Experienced in maintaining high-impact mobile applications and developer SDKs.