skip to content

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%

answer

  1. deepest view wins by default
  2. capture runs root to leaf first
  3. the first true wins
  4. steal on move, not on start
  5. only the holder's ancestors are asked

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.

solid answer

~40 s

Should-set questions run in two phases. First a capture phase goes from the root down to the touched view and calls `onStartShouldSetResponderCapture` (or `onMoveShouldSetResponderCapture` for moves); then a bubble phase goes from the touched view back up and calls `onStartShouldSetResponder` or `onMoveShouldSetResponder`. The first handler that returns true wins. With bubble handlers only, the deepest view, such as a colour swatch, gets the touch, which keeps controls usable. A parent that returns true from the start capture handler takes every touch and starves its children, so a greedy start capture is usually a bug. The better pattern is to steal on move: return true from `onMoveShouldSetResponderCapture` only once the gesture proves to be yours, such as a second finger for a two-finger page turn. Stealing still asks the holder's `onResponderTerminationRequest`, which can refuse.

code

tsx · 17 lines
tsx
import {type ReactNode} from 'react';
import {StyleSheet, View} from 'react-native';

type Props = {children: ReactNode; onNextPage: () => void};

export function PageTurner({children, onNextPage}: Props) {
  return (
    <View
      style={styles.page}
      onMoveShouldSetResponderCapture={e => e.nativeEvent.touches.length >= 2}
      onResponderRelease={onNextPage}>
      {children}
    </View>
  );
}

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

go deeper

for a junior

Recall that the deepest view wins by default and that Capture handlers let a parent ask first.

for a middle

Explain the two-phase walk, why the first true wins, and why stealing on move is safer than claiming on start.

for a senior

Show you can untangle nested gestures: which ancestors are asked once a responder exists, and how the holder's termination request interacts with a parent's capture.

for a principal

Weigh negotiating nested gestures in the responder system against moving the screen to explicit gesture composition, and set a rule for when each is acceptable.

## Two phases of asking When a touch starts or moves, React Native's responder system has to find out which view wants it. It asks in **two phases**, walking the path between the root view and the touched view: 1. **Capture phase**, from the root **down** to the touched view: calls `onStartShouldSetResponderCapture` on a touch start, or `onMoveShouldSetResponderCapture` on a move. 2. **Bubble phase**, from the touched view back **up** to the root: calls `onStartShouldSetResponder` or `onMoveShouldSetResponder`. The walk stops at the **first handler that returns `true`**, and that view becomes the candidate responder. ## The default: the deepest view wins If nobody implements a Capture handler, only the bubble phase matters, and the touched view is asked **first**. So when a parent and a child both return `true` from `onStartShouldSetResponder`, the **child** wins. This is deliberate: it guarantees that buttons, swatches and other small controls inside a larger interactive area keep working. ## Taking the touch as a parent | Handler | Phase | Typical use | Risk | |---|---|---|---| | `onStartShouldSetResponderCapture` | capture, touch start | a modal backdrop that must swallow every touch | children never get a tap | | `onMoveShouldSetResponderCapture` | capture, move | a container that takes over once a drag or second finger proves intent | a threshold that is too low steals taps | | `onStartShouldSetResponder` | bubble, touch start | the view's own claim when no child wants it | none; the default model | | `onMoveShouldSetResponder` | bubble, move | claim a touch that started on a child that did not claim it | none; the default model | The capture variants exist so that an **outer** view can win against the views between it and the touch. PanResponder exposes the same four as `onStartShouldSetPanResponderCapture`, `onMoveShouldSetPanResponderCapture` and the two non-capture names, with `gestureState` available to base the decision on. ## When a responder already exists Once some view holds the touch, the system narrows who is asked: - It finds the **lowest common ancestor** of the current responder and the view under the finger, and asks only that ancestor and the views **above** it, in both phases. - When the finger is on the responder itself, the responder is skipped and only its ancestors are asked. - A **sibling** of the responder therefore cannot steal the touch; only an ancestor can. - If an ancestor says yes, the holder's `onResponderTerminationRequest` still decides. Returning `false` there sends the ancestor `onResponderReject`. ## Example: a two-finger page turn over a drawing canvas In a kids' sketch app, one finger draws and two fingers swipe to the next page. The canvas claims on start; the page container claims with a **move capture** handler only when two touches are down. Single-finger strokes and taps never reach the container's claim, and a second finger makes it take over. Because the canvas has no termination request handler here, it lets go and receives `onResponderTerminate`, which must discard the stroke that the first finger had begun. ## Pitfalls - A start-capture handler that returns `true` unconditionally on a screen container: every child button stops responding. - A move-capture threshold of a few points: shaky taps become drags and buttons feel broken. - Forgetting that the holder can refuse: a canvas returning `false` from its termination request blocks the page turn entirely. - Doing heavy work in capture handlers: they run for many views on the JavaScript thread before any view is chosen. ## Deciding where each claim belongs A short checklist when two gestures overlap: 1. Does the inner view need taps? Then the outer view must not use start capture. 2. Can the outer gesture be recognised from movement alone, such as distance, direction or finger count? Put that test in move capture. 3. Must the inner gesture survive once it starts, such as a stroke in progress? Give the inner view a termination request that returns `false` while it is busy. 4. Is the outer gesture more important than anything inside it, such as a full-screen dismiss? Only then consider start capture. Working through these four in order usually produces a handler set that behaves the same way every time, instead of one that depends on which view happened to be touched first.

  • Why is returning true from onStartShouldSetResponderCapture on a screen container usually a bug in React Native?
    Capture runs before any child is asked, and the first true wins, so the container becomes responder for every touch that starts inside it. Buttons, swatches and text inputs below it never see a touch. Unless the container is meant to swallow everything, like a backdrop, claim on move with a threshold instead.
  • Can a sibling View take a touch away from the current responder in React Native?
    No. While a responder exists, the system asks only the lowest common ancestor of the responder and the touched view, plus the views above it. A sibling is below that ancestor, so it is never asked. Only an ancestor can claim the touch, and even then the holder's termination request can refuse.

saying these in an interview costs you the question

  • The outermost View gets the touch first by default
  • Capture handlers run after the bubble handlers as a fallback
  • A parent's capture handler takes the touch no matter what the holder answers
  • Every View on screen is asked on every move while a gesture is active
  • Capture variants exist only on PanResponder, not on a plain View