In React Native, how do you wire PanResponder.create to a View so a finger can draw, and why keep the instance in useRef?
answer
- a config object of callbacks
- spread panHandlers onto the View
- unset should-set callbacks mean false
- one gestureState per create() call
- stable instance, changing values via refs
basics
~20 sCall 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.
solid answer
~50 s`PanResponder.create(config)` takes callbacks such as `onStartShouldSetPanResponder`, `onPanResponderGrant`, `onPanResponderMove` and `onPanResponderRelease`, and returns an object whose `panHandlers` you spread onto a `View`. Each callback receives the touch event plus a `gestureState` carrying `dx`, `dy`, `moveX`, `moveY`, `vx` and `vy`. The view only claims the touch if `onStartShouldSetPanResponder` (or the move variant) returns true; leave both out and the generated handlers return false, so nothing ever fires. For a drawing canvas you read `locationX`/`locationY` on grant and move, and finish the stroke on release or terminate. Create the instance once, with `useRef(...).current` or `useMemo`: every `create()` call allocates its own `gestureState`, and React Native dispatches to the handlers from the latest render, so rebuilding it on each render hands the next move to handlers that never saw the grant. Because the handlers are created once, read changing values such as the brush colour through a ref.
code
tsx · 42 linesimport {useRef, useState} from 'react';
import {PanResponder, StyleSheet, View} from 'react-native';
type Point = {x: number; y: number};
export function SketchPad() {
const [points, setPoints] = useState<Point[]>([]);
const pan = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: evt => {
const {locationX, locationY} = evt.nativeEvent;
setPoints(prev => [...prev, {x: locationX, y: locationY}]);
},
onPanResponderMove: evt => {
const {locationX, locationY} = evt.nativeEvent;
setPoints(prev => [...prev, {x: locationX, y: locationY}]);
},
}),
).current;
return (
<View style={styles.canvas} {...pan.panHandlers}>
{points.map((p, i) => (
<View key={i} style={[styles.dot, {left: p.x - 4, top: p.y - 4}]} />
))}
</View>
);
}
const styles = StyleSheet.create({
canvas: {flex: 1, backgroundColor: 'white'},
dot: {
position: 'absolute',
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: 'purple',
pointerEvents: 'none',
},
});go deeper
Recall the shape: PanResponder.create with callbacks, spread panHandlers on a View, and return true from onStartShouldSetPanResponder or nothing fires.
Explain what gestureState holds, why dx is accumulated since the grant, and why the instance must survive re-renders with useRef or useMemo.
Show you have debugged it: the line that jumps because locationX follows the touched child, stale closures over props, and a re-render that swaps handlers mid-stroke.
Frame when PanResponder is still acceptable for a team's simple drags versus standardising on a UI-thread gesture library, and what that means for consistency across screens.
## What PanResponder is `PanResponder` is a JavaScript helper that ships in `react-native`. It sits on top of the **gesture responder system**: the family of props every `View` accepts, such as `onStartShouldSetResponder`, `onResponderGrant`, `onResponderMove` and `onResponderRelease`. PanResponder reconciles several touches into one gesture and passes a second argument, **`gestureState`**, to each of your callbacks so you do not have to compute distances and velocities yourself. `PanResponder.create(config)` returns an object with a **`panHandlers`** property. That property is a ready-made set of responder props, and you spread it onto the view that should receive the gesture: `<View {...pan.panHandlers} />`. It works the same on a plain `View` and on an `Animated.View`. ## The callbacks a drawing canvas needs | Callback | When it runs | Use in a kids' sketch canvas | |---|---|---| | `onStartShouldSetPanResponder` | a touch starts on the view; return `true` to claim it | return `true` so a finger-down starts a stroke | | `onMoveShouldSetPanResponder` | a move happens while the view is not the responder | claim a touch that started elsewhere | | `onPanResponderGrant` | the view has become the responder | begin a new stroke at the touch point | | `onPanResponderMove` | the finger moves | append a point to the stroke | | `onPanResponderRelease` | the last finger lifts | commit the stroke | | `onPanResponderTerminate` | the touch was taken away | discard or commit the partial stroke | Defaults matter here: - If you omit **both** `onStartShouldSetPanResponder` and `onMoveShouldSetPanResponder`, the generated handlers return `false`. The view never becomes the responder, and `onPanResponderGrant` and `onPanResponderMove` never fire. This is the most common "my PanResponder does nothing" bug. - If you omit `onPanResponderTerminationRequest`, it is treated as `true`: the view gives the touch up whenever another view asks. - If you omit `onShouldBlockNativeResponder`, it is treated as `true`. ## gestureState versus nativeEvent `gestureState` describes the gesture as a whole: - `dx` / `dy` — **accumulated** distance since the responder was granted, not since the previous event. - `moveX` / `moveY` — latest screen coordinates of the recently moved touch. - `x0` / `y0` — screen coordinates at the grant. - `vx` / `vy` — current velocity. - `numberActiveTouches` — touches currently on screen. The event's `nativeEvent` describes one touch. `locationX` and `locationY` are relative to the **element that was touched**, and `pageX` and `pageY` are relative to the root view. For drawing you want coordinates inside the canvas, and `locationX` gives you that only while the canvas itself is the touched element. If the finger lands on a child, such as a dot drawn earlier, `locationX` is suddenly relative to that child and the line jumps. Giving the dots `pointerEvents: 'none'` in their style keeps the canvas as the target. ## Why the instance lives in useRef 1. Each `PanResponder.create()` call allocates its **own `gestureState`** object, and the handlers it returns close over it. 2. When React Native dispatches a responder event, it reads the handler from the view's **current props**, so after a re-render the next move goes to whichever `panHandlers` were rendered last. 3. If `create()` runs in the component body, a re-render in the middle of a stroke (for example because the move handler calls `setState`) swaps in new handlers whose `gestureState` never saw the grant: `x0`/`y0` read `0` and `dx`/`dy` restart from zero. So create it once: `useRef(PanResponder.create({...})).current`, or `useMemo(() => PanResponder.create({...}), [])`. The `useRef` form still evaluates its argument on every render and throws the result away; `useMemo` or a lazy ref avoids that waste. The trade-off is **stale closures**. Handlers created once see the props and state of the first render. Read values that change, such as the brush colour or an eraser toggle, from a ref you update on each render, and use functional `setState` updaters. ## Version note Older documentation says PanResponder holds an "interaction handle" to hold back `InteractionManager` work during a gesture. `InteractionManager` was removed from core in React Native 0.87, and the instance's `getInteractionHandle()` returns `null`, so that behaviour is no longer something to rely on. ## Common mistakes - **Forgetting the claim.** Spreading `panHandlers` without a should-set callback that returns `true` produces a view that silently ignores touches. - **Rebuilding on every render.** Calling `PanResponder.create` in the component body works in a quick demo and breaks as soon as the move handler triggers a re-render. - **Reading `dx` as a per-event delta.** Drawing code that adds `dx` to the previous point overshoots further with every move; use the event's coordinates or treat `dx` as the offset from the grant. - **Ignoring terminate.** A stroke that ends only on release leaves half-drawn state behind whenever the touch is taken away. - **Mixing coordinate systems.** `moveX` and `x0` are screen coordinates, while `locationX` is element-relative; mixing them draws the line in the wrong place.
- Why does a PanResponder callback created inside useRef keep seeing the brush colour from the first render?The handlers are closures created once, on the first render, so they captured that render's props and state. Later renders build new values, but the stored handlers never see them. Keep a ref such as `colourRef`, assign `colourRef.current = colour` on every render, and read the ref inside the callback; for list-like state, use a functional `setState` updater instead of reading the captured array.
- In a React Native PanResponder canvas, when would you use pageX instead of locationX from the event?`locationX` is relative to whichever element was touched, so it breaks when children can become the touch target. `pageX` is relative to the root view and never depends on the target, so you subtract the canvas's own page position, measured once with `measure()` on layout. Use `locationX` when the canvas is always the target, for example because its children have `pointerEvents: 'none'`.
saying these in an interview costs you the question
- PanResponder starts working as soon as panHandlers are spread, with no should-set callback
- Calling PanResponder.create in the component body is fine because React reuses the old instance
- gestureState.dx is the distance moved since the previous move event
- locationX is always relative to the View that owns the panHandlers
- PanResponder needs a class component to keep its instance stable