In React Native Gesture Handler 3, what separates onBegin, onActivate, onDeactivate and onFinalize, and what does event.canceled tell you?
answer
- began is not active
- paired guarantees for both pairs
- Deactivate only after Activate
- canceled replaced success, inverted
- onStart and onEnd were renamed
basics
~20 sonBegin fires when a handler starts tracking a touch and onFinalize always follows it; onActivate fires when activation criteria are met and onDeactivate always follows that. event.canceled is true when the gesture failed or was interrupted.
solid answer
~40 sA Gesture Handler gesture is a state machine: `UNDETERMINED`, `BEGAN`, `ACTIVE`, then `END`, `FAILED` or `CANCELLED`. `onBegin` fires on `BEGAN`, when the handler starts receiving the touch but has not yet recognised anything; it is the place for an immediate press highlight. `onActivate` fires when the activation criteria are met, such as a pan passing `minDistance`, and `onUpdate` streams data after that. `onDeactivate` runs only if the gesture activated, and `onFinalize` runs whenever `onBegin` did, so pressed-state cleanup belongs in `onFinalize`. Both end callbacks receive `event.canceled`: false when the gesture completed, true when it failed or was interrupted, for example by a competing gesture. Gesture Handler 3 renamed version 2's `onStart` and `onEnd` to `onActivate` and `onDeactivate` and replaced the `success` argument with `canceled`, which is inverted.
code
tsx · 29 linesimport {GestureDetector, useTapGesture} from 'react-native-gesture-handler';
import Animated, {useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
export function PhotoThumb({onOpen}: {onOpen: () => void}) {
const dim = useSharedValue(1);
const tap = useTapGesture({
runOnJS: true,
onBegin: () => {
dim.value = 0.6;
},
onDeactivate: e => {
if (!e.canceled) {
onOpen();
}
},
onFinalize: () => {
dim.value = 1;
},
});
const style = useAnimatedStyle(() => ({opacity: dim.value}));
return (
<GestureDetector gesture={tap}>
<Animated.View style={[{width: 96, height: 96, backgroundColor: 'gray'}, style]} />
</GestureDetector>
);
}go deeper
Recall the order: onBegin, onActivate, onUpdate, onDeactivate, onFinalize, and that canceled marks an unsuccessful end.
Explain the two guaranteed pairs and why feedback started in onBegin must be undone in onFinalize, not onDeactivate.
Catch the migration and logic bugs: inverted success/canceled checks, and success paths that fire on interrupted gestures.
Decide how shared gesture components expose these states so product code cannot confuse began, active and completed.
## A gesture is a state machine Every gesture in Gesture Handler moves through a small set of **states**, and the callbacks are hooks into the transitions: | State | Meaning | Callback | |---|---|---| | `UNDETERMINED` | initial state, no touch | none | | `BEGAN` | receiving touches, criteria not met yet | `onBegin` | | `ACTIVE` | the gesture is recognised | `onActivate` on entry, `onUpdate` with new data | | `END` | completed successfully | `onDeactivate` and `onFinalize`, `canceled: false` | | `FAILED` | did not recognise the gesture | `onDeactivate` only if it had been active, then `onFinalize`, `canceled: true` | | `CANCELLED` | interrupted by the system or another gesture | `onDeactivate` only if it had been active, then `onFinalize`, `canceled: true` | ## Two guaranteed pairs The documentation gives two guarantees that shape where code belongs: 1. If `onBegin` was called, `onFinalize` **will** be called later. 2. If `onActivate` was called, `onDeactivate` **will** be called later, and it runs **before** `onFinalize`. So: - **Visual feedback that starts on touch-down** (dimming a thumbnail) goes in `onBegin` and is undone in `onFinalize`, because a touch can begin and then fail without ever activating. - **Work tied to a recognised gesture** (saving the new zoom level) goes in `onActivate`/`onUpdate` and is completed in `onDeactivate`. ## What event.canceled means `onDeactivate` and `onFinalize` receive a `GestureEndEvent`, which adds a boolean **`canceled`** to the usual event data: - `canceled: false` — the gesture completed. - `canceled: true` — in `onFinalize`, the handler **failed to activate or was interrupted**; in `onDeactivate`, it was interrupted after activating. A double-tap that turns into a drag, or a pan cancelled because a pager won, both finish with `canceled: true`. Code that treats every `onDeactivate` as success, such as committing a zoom or opening a photo, fires on interruptions too. ## What changed in Gesture Handler 3 | Gesture Handler 2 | Gesture Handler 3 | |---|---| | `onStart` | `onActivate` | | `onEnd` | `onDeactivate` | | `onTouchesCancelled` | `onTouchesCancel` | | `success` second argument to `onEnd`/`onFinalize` | `event.canceled`, **inverted** | | `onChange` with `changeX` | removed; `changeX` is available in `onUpdate` | | `state` / `oldState` on events | removed; use the callbacks | The inversion is the usual migration bug: `if (success)` must become `if (!event.canceled)`. ## Touch callbacks versus gesture callbacks Besides the state callbacks, every gesture accepts low-level `onTouchesDown`, `onTouchesMove`, `onTouchesUp` and `onTouchesCancel`, which report raw pointers, possibly batched, regardless of whether the gesture activates. They are for manual gestures and unusual cases; ordinary code uses the state callbacks. ## In the photo viewer A single tap toggles the viewer's toolbar. Using `onDeactivate` with a `canceled` check means a tap that turned into a pan, or lost to a double-tap, never toggles it. Dimming the photo while a finger is down uses `onBegin` and `onFinalize`, so the dim always clears, even when the tap fails. ## Choosing the right callback - **Immediate press feedback:** `onBegin`, undone in `onFinalize`. - **Work that should start only once the gesture is recognised,** such as hiding a hint: `onActivate`. - **Continuous tracking** of translation, scale or rotation: `onUpdate`, which now also carries the `change*` values that version 2 delivered through `onChange`. - **Committing a result,** such as saving the zoom or opening the photo: `onDeactivate`, guarded with `!event.canceled`. - **Cleanup that must always run:** `onFinalize`. - **Raw pointer handling** for a custom manual gesture: the touch callbacks, with the global `GestureStateManager` to activate or fail the gesture yourself. The most common review comment on gesture code is a success path in `onDeactivate` or `onFinalize` without a `canceled` check.
- In Gesture Handler 3, why should a pressed-state highlight be cleared in onFinalize rather than onDeactivate?A touch can begin and then fail without ever activating, for example when a tap turns into a drag. In that case `onBegin` ran but `onActivate` and `onDeactivate` never do. `onFinalize` is guaranteed after `onBegin`, so it is the only place that clears the highlight on every path.
- When migrating a Gesture Handler 2 onEnd((e, success) => ...) callback to version 3, what is the usual mistake?Renaming `onEnd` to `onDeactivate` but translating `success` directly. Version 3 exposes `event.canceled`, which is the inverse, so `if (success)` must become `if (!event.canceled)`. Copying the condition unchanged runs the success path exactly when the gesture was interrupted.
saying these in an interview costs you the question
- onDeactivate runs for every touch, even when the gesture never activated
- event.canceled: true means the gesture completed successfully
- onBegin means the activation criteria have already been met
- onStart and onEnd are still the version 3 names in the hook API
- A failed tap still calls onActivate before failing