skip to content

Responder System

A view claims a touch with onStartShouldSetResponder, a parent can take it first in the capture phase, and PanResponder turns moves into gestureState. Interviewers ask why it lags when JS is busy.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In React Native's gesture responder system, how does a View become the touch responder, and which callbacks fire as it keeps or loses the touch?

level: middleimportance: must knowfreq 40%

answer

  1. ask, then grant or reject
  2. start versus move should-set
  3. Grant shows feedback, Release commits
  4. a missing TerminationRequest means let go
  5. the OS can Terminate without asking

basics

~10 s

A View returns true from onStartShouldSetResponder or onMoveShouldSetResponder, then gets onResponderGrant, or onResponderReject if the holder refuses. It receives onResponderMove, and ends with onResponderRelease, or onResponderTerminate if the touch is taken away.

solid answer

~50 s

Only one view is the responder for a touch at a time. A view asks by returning true from `onStartShouldSetResponder` at touch start, or from `onMoveShouldSetResponder` on moves while it is not the responder. If it wins it gets `onResponderGrant`, the moment to show feedback; if the current holder will not let go it gets `onResponderReject`. While it holds the touch it receives `onResponderMove`, and `onResponderRelease` when the last finger on it lifts. When another view asks for the touch, the holder's `onResponderTerminationRequest` decides: returning true releases it, and a missing handler also releases. Losing the touch fires `onResponderTerminate`, which can also arrive with no request at all when the OS takes the touch, such as iOS Control Center. In a sketch app, Release commits the stroke, and Terminate must clean it up as well or a half-drawn line stays behind.

code

tsx · 38 lines
tsx
import {useRef, useState} from 'react';
import {StyleSheet, View, type GestureResponderEvent} from 'react-native';

type Point = {x: number; y: number};

export function StickerCanvas() {
  const [strokes, setStrokes] = useState<Point[][]>([]);
  const current = useRef<Point[]>([]);

  const point = (e: GestureResponderEvent): Point => ({
    x: e.nativeEvent.locationX,
    y: e.nativeEvent.locationY,
  });

  return (
    <View
      style={styles.canvas}
      onStartShouldSetResponder={() => true}
      onResponderGrant={e => {
        current.current = [point(e)];
      }}
      onResponderMove={e => {
        current.current.push(point(e));
      }}
      onResponderRelease={() => {
        const done = current.current;
        current.current = [];
        setStrokes(prev => [...prev, done]);
      }}
      onResponderTerminationRequest={() => false}
      onResponderTerminate={() => {
        current.current = [];
      }}
    />
  );
}

const styles = StyleSheet.create({canvas: {flex: 1}});

go deeper

for a junior

Recall the order: a should-set handler returns true, Grant fires, Move streams positions, and Release or Terminate ends it.

for a middle

Explain the handoff: the holder's termination request decides, a missing handler means let go, and the challenger gets Grant or Reject.

for a senior

Show production awareness: OS cancellations arrive as Terminate without a request, so gesture state must be cleaned up there as well as on Release.

for a principal

Discuss how a shared gesture policy keeps nested interactive regions from fighting, and when it is cheaper to redesign the layout than to negotiate touches.

## One responder per touch React Native's **gesture responder system** decides which view owns a touch. It runs in JavaScript inside the renderer, and every `View` can take part through a set of props. The central rule is that there is **exactly one responder** at a time: the view that receives the move and end callbacks. Views negotiate for that role without knowing anything about their parents or children, which is what lets a button inside a card inside a scrolling screen all behave sensibly. ## Asking for the touch Two **should-set** handlers ask a view whether it wants to become responder: - `onStartShouldSetResponder(evt)` — called when a touch starts. Return `true` to claim it. - `onMoveShouldSetResponder(evt)` — called on a touch move while the view is **not** the responder. Return `true` to claim a touch that is already under way. Both must return a boolean synchronously. Each also has a **Capture** variant that lets a parent ask first; ordering between parent and child is a separate topic from the lifecycle below. ## Winning, holding and losing | Callback | Fires when | Notes | |---|---|---| | `onResponderGrant` | the view has just become responder | show feedback; a `true` return asks the platform to block native responders, which Android acts on | | `onResponderReject` | the view asked, but the holder refused | the view gets nothing further for this touch | | `onResponderMove` | the finger moves | the stream of positions | | `onResponderRelease` | the last touch on the view lifts | the normal, successful end | | `onResponderTerminationRequest` | another view asks for the touch | return `true` to let go; if absent, the touch is let go | | `onResponderTerminate` | the touch has been taken away | after an accepted request, or by the OS with no request | There are also `onResponderStart` and `onResponderEnd`, which fire on the responder as additional fingers go down or lift during a multi-touch gesture. ## The handoff, step by step When a second view wants a touch that someone already holds: 1. The challenger's should-set handler returns `true`. 2. The system sends `onResponderTerminationRequest` to the **current holder**. 3. If the holder returns `true`, or has no handler, the challenger receives `onResponderGrant` and the holder receives `onResponderTerminate`. 4. If the holder returns `false`, the challenger receives `onResponderReject` and the holder keeps the touch. The request is not the only way to lose a touch. When the platform **cancels** the touch, for example because the user pulled down iOS Control Center or Notification Center, the responder gets `onResponderTerminate` directly. Nobody asked it, and it cannot refuse. ## Designing the handlers for a sketch canvas - **Claim on start.** Return `true` from `onStartShouldSetResponder` so that a single tap draws a dot, not only a drag. - **Grant:** record the first point and show a brush preview. - **Move:** append a point. - **Release:** commit the stroke to the drawing. - **Terminate:** decide deliberately. Discard the partial stroke, or commit what exists, but always clear the in-progress state. A canvas that only listens to `onResponderRelease` leaves a dangling stroke every time the OS interrupts. - **TerminationRequest:** return `false` while a stroke is in progress if parents such as a page-swipe container must not steal it mid-line. ## Common mistakes - Treating `onResponderRelease` as the only end of a gesture. - Assuming a view that omits `onResponderTerminationRequest` keeps the touch; the absence means yes. - Doing expensive work in should-set handlers. They are asked on many views for every touch start and run on the JavaScript thread. - Expecting two views to receive `onResponderMove` for one touch. Only the responder does. ## What the event carries Every handler receives a synthetic touch event whose `nativeEvent` holds: - `locationX` / `locationY` — the touch position relative to the element that was touched. - `pageX` / `pageY` — the position relative to the root view. - `identifier` — an ID for this touch, stable while the finger stays down. - `timestamp` — useful for computing velocity yourself. - `touches` and `changedTouches` — all current touches, and the ones that changed since the last event. These are enough to build a stroke directly from the raw props. PanResponder adds an accumulated `gestureState` on top, but the lifecycle it wraps is exactly the one described here.

  • In React Native's responder system, what is the practical difference between onResponderRelease and onResponderTerminate for a stroke in progress?
    `onResponderRelease` means the user finished: the last finger on the view lifted, so the stroke is complete and should be committed. `onResponderTerminate` means the touch was taken away, either by another view after an accepted termination request or by the OS cancelling the touch. The stroke is incomplete, so you discard it or commit it knowingly, but you must reset the in-progress state either way.
  • What does returning true from onResponderGrant do in React Native?
    The return value tells the system whether to block native responders while this JS view holds the touch. On Android, React Native then stops the view's parents from intercepting the touch, so a native scroll view cannot take it. PanResponder feeds this value from `onShouldBlockNativeResponder`, which defaults to true, and its docs mark it as Android-only.

saying these in an interview costs you the question

  • Several Views can be the responder for the same touch at once
  • A View that omits onResponderTerminationRequest never gives up the touch
  • onResponderTerminate only fires after the holder agreed to a termination request
  • onResponderRelease fires whenever any finger lifts during a multi-touch gesture
  • onMoveShouldSetResponder keeps firing on the View that already holds the touch
open as a page

In React Native, why does a PanResponder drawing stroke trail behind the finger when the JavaScript thread is busy, and how do you fix it?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Every responder decision and PanResponder callback runs on the JavaScript thread, so moves wait behind other JS work and a re-render before the stroke appears. Keep move handlers cheap, or move the gesture to the UI thread.

open as a page

In React Native, how do you wire PanResponder.create to a View so a finger can draw, and why keep the instance in useRef?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Call PanResponder.create with onStartShouldSetPanResponder returning true plus move and release callbacks, then spread its panHandlers onto a View. Keep the instance in useRef so re-renders do not swap in new handlers and a fresh gestureState mid-stroke.

open as a page

In React Native's responder system, when a parent View and its child both want a touch, which wins, and how do Capture handlers change it?

level: middleimportance: should knowfreq 30%

basics

~10 s

By default the deepest View wins, because should-set handlers bubble up from the touched child. A parent can claim first with onStartShouldSetResponderCapture or onMoveShouldSetResponderCapture, which run top-down before the bubble phase.

open as a page

In React Native, a PanResponder drawing canvas inside a vertical ScrollView keeps losing strokes to scrolling; why, and how do you stop it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The ScrollView competes on two layers: it asks for the JS responder once it scrolls, and the native scroll gesture can cancel the touch. Refuse in onPanResponderTerminationRequest, keep the Android block default, and disable scrollEnabled while a stroke is in progress.

open as a page

In a React Native PanResponder, why can reading evt.nativeEvent or gestureState inside a setState updater or setTimeout give null or zeros?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Both objects are reused. React Native's responder events are pooled, so nativeEvent is nulled once the handler returns, and PanResponder mutates one gestureState in place and resets it to zeros after release or terminate. Copy the numbers out synchronously.

open as a page