Composition
useGestures relates gestures to one another. You pass a list and one mode.
import { GestureDetector, useGestures } from '@rootnative/impulse'
const { gesture } = useGestures([tap, drag], { mode: 'simultaneous' })
return (
<GestureDetector gesture={gesture}>
<View />
</GestureDetector>
)
Composition is data, not nesting
RNGH expresses the same relation as a nested call:
Gesture.Simultaneous(Gesture.Race(tap, doubleTap), drag)
You read that backwards to learn what wins, and it gains one level of nesting per relation.
Impulse takes a flat list plus the relation that holds over it. The result is itself a member, so you nest only when you mean to:
const { gesture } = useGestures(
[useGestures([doubleTap, tap], { mode: 'exclusive' }), drag],
{ mode: 'simultaneous' },
)
Precedence reads left to right, and depth is a choice, not a consequence.
The three modes
| Mode | What happens | Use it for |
|---|---|---|
race | The first member to activate wins and cancels the rest. | Mutually exclusive readings of one touch. |
simultaneous | Every member recognizes independently. | A drag and a long press on one card. |
exclusive | Impulse tries the members in order. A later member activates only after every earlier member failed. | A tap that must wait for a double tap. |
Order matters for exclusive only. race and simultaneous ignore it.
A tap and a double tap are exclusive
This is the one case that is easy to get wrong, so it is worth stating on its own.
// Correct. The double tap comes first.
const { gesture } = useGestures([doubleTap, tap], { mode: 'exclusive' })
A single tap recognizes on the first release. In a race it therefore wins
every time, and the double tap never fires. exclusive is what makes the
single tap wait to learn whether a second tap is coming.
A composed single tap can't report for maxDelay milliseconds, 500 ms by
default. Every ordinary tap pays it. See the useDoubleTap page for the
trade.
Members
A member is anything that carries a gesture:
- any Impulse hook result:
tap,drag,useGestures(…) - a bare RNGH gesture from
@rootnative/impulse/gesture-handler
Pass the hook result itself, not result.gesture. Both work, but the result
is memoised, so you can depend on it.
Coexistence options do not belong here
useGestures doesn't accept alongside, blocks, or deferTo. Put them on
the member hooks.
// Right. The relation sits on the member that has it.
const drag = useDrag({ axis: 'y', deferTo: scrollRef })
const { gesture } = useGestures([tap, drag], { mode: 'simultaneous' })
RNGH's three external-gesture relations are methods on a single gesture. A composed gesture doesn't have them. Impulse could apply a relation to each member instead. But that would mutate gesture objects that another hook already built and memoised, and that breaks the rule that a gesture is configured exactly once. So the options stay on the members.
Tell us if that is awkward in a real app. It is recorded as an open question, not a settled one.
Read Coexistence for what the three options do.
Two dev warnings
Both are development-only and print at most once.
An empty list. The composition recognizes nothing, so the
<GestureDetector> that holds it is inert. Build the list conditionally
around the hook call. Don't pass an empty list.
An unknown mode. TypeScript can't reach it. JavaScript can. Impulse falls
back to race and names the valid modes. Otherwise a misspelled mode would
fail as "the gesture doesn't work".
Accessibility
A composition is as reachable as its members: a screen reader and a keyboard see none of it. Each member hook names its own fallback, and a composition needs the union of them.