Skip to main content

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​

ModeWhat happensUse it for
raceThe first member to activate wins and cancels the rest.Mutually exclusive readings of one touch.
simultaneousEvery member recognizes independently.A drag and a long press on one card.
exclusiveImpulse 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.

The price is latency

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:

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' })
This is a limitation, not a preference

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.