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?
answer
- ask, then grant or reject
- start versus move should-set
- Grant shows feedback, Release commits
- a missing TerminationRequest means let go
- the OS can Terminate without asking
basics
~10 sA 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 sOnly 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 linesimport {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
Recall the order: a should-set handler returns true, Grant fires, Move streams positions, and Release or Terminate ends it.
Explain the handoff: the holder's termination request decides, a missing handler means let go, and the challenger gets Grant or Reject.
Show production awareness: OS cancellations arrive as Terminate without a request, so gesture state must be cleaned up there as well as on Release.
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