skip to content

In React Native Gesture Handler 3, what separates onBegin, onActivate, onDeactivate and onFinalize, and what does event.canceled tell you?

level: middleimportance: should knowfreq 35%

answer

  1. began is not active
  2. paired guarantees for both pairs
  3. Deactivate only after Activate
  4. canceled replaced success, inverted
  5. onStart and onEnd were renamed

basics

~20 s

onBegin 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 s

A 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 lines
tsx
import {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

for a junior

Recall the order: onBegin, onActivate, onUpdate, onDeactivate, onFinalize, and that canceled marks an unsuccessful end.

for a middle

Explain the two guaranteed pairs and why feedback started in onBegin must be undone in onFinalize, not onDeactivate.

for a senior

Catch the migration and logic bugs: inverted success/canceled checks, and success paths that fire on interrupted gestures.

for a principal

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