Skip to main content

Installation

Platforms​

Impulse runs where react-native-gesture-handler runs. This table shows what we checked on each platform.

PlatformStatus
iOSSupported (alpha). An XCUITest sweep on an iOS simulator drove every intent. No physical device ran it.
AndroidSupported (alpha). An injected-touch sweep on an Android emulator drove every intent. No physical device ran it.
WebPartial. useTap, useDoubleTap, useLongPress, and useDrag work under a mouse in a desktop browser. usePan, useSwipe, usePinch, and useRotate are not verified. No automated test covers web.
macOS, WindowsNot tested.
ElectronNot tested. Expo has no Electron target.

The sweeps check the mechanics only. Nobody has tested the feel of a gesture on any platform.

For web, an Expo app also needs react-dom and react-native-web:

npx expo install react-dom react-native-web

The example app runs on web with react-native-web 0.21, react-dom 19.3.0, and Gesture Handler 3.3. Web behaviour lists each intent, the differences from native, and the open questions.

Install the package​

npm install @rootnative/impulse

Install the peers​

Impulse declares five peer dependencies and bundles none of them.

In an Expo app, let Expo pick the versions that match your SDK:

npx expo install react-native-gesture-handler react-native-reanimated react-native-worklets

In a bare React Native app, install them with your package manager. Then run npx pod-install for iOS.

PeerRange
react>=19.3.0 <20.0.0
react-native>=0.88.0 <0.89.0
react-native-gesture-handler>=3.3.0 <4.0.0
react-native-reanimated>=4.7.0 <4.8.0
react-native-worklets>=0.13.0 <0.14.0
Why the Reanimated range is so narrow

Reanimated 4 and react-native-worklets are version-locked to each other. An open upper bound lets a package manager pair a new Reanimated with an old worklets build, and that pair doesn't install. The two narrow ranges are on purpose. Don't widen them.

Impulse is developed against Expo SDK 58: React 19.3.0, React Native 0.88.1, Gesture Handler 3.3.0, Reanimated 4.7.1, and Worklets 0.13.0. An Expo SDK 57 app uses 0.0.0-alpha.1.

Impulse builds its gestures with the builder API of Gesture Handler (Gesture.Pan()). Gesture Handler 3 marks that API as deprecated, and it still runs it. A gesture from the Gesture Handler 3 hook API (usePanGesture) doesn't compose with an Impulse gesture. A relation target must be LegacyScrollView or LegacyFlatList. See Coexistence.

Add the root view​

Render <GestureHandlerRootView> above every component that uses a gesture. Wrap your app at its entry point.

import { GestureHandlerRootView } from 'react-native-gesture-handler'

export default function App() {
return (
<GestureHandlerRootView style={styles.root}>
<Navigation />
</GestureHandlerRootView>
)
}

const styles = StyleSheet.create({
root: { flex: 1 },
})
A missing root view fails silently on web and in a release build

Without it, your gestures never fire. In development on iOS and Android, Gesture Handler 3 throws: GestureDetector must be used as a descendant of GestureHandlerRootView. The same error stops a Jest test that renders a <GestureDetector> outside the root view. See Testing.

On web and in a release build, nothing throws, nothing warns, and the console stays empty. If a gesture does nothing at all, check this first.

Give the root view flex: 1, or the area below it receives no touches.

Babel​

An Expo app needs no Babel change. babel-preset-expo adds react-native-worklets/plugin on its own as soon as the package is installed.

In a bare React Native app, add the plugin by hand. It must be the last entry in the list:

// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-worklets/plugin'],
}

Restart Metro with a cleared cache after you change this file: npx expo start --clear, or npx react-native start --reset-cache.

Import what you use​

The package entry carries every hook, plus GestureDetector:

import { GestureDetector, useDrag, useTap } from '@rootnative/impulse'

Each hook also has its own subpath, so an app that uses one gesture does not ship the set:

import { useDrag } from '@rootnative/impulse/drag'
SubpathWhat it carries
@rootnative/impulseEvery hook, the types, and GestureDetector.
@rootnative/impulse/tapuseTap
@rootnative/impulse/double-tapuseDoubleTap
@rootnative/impulse/long-pressuseLongPress
@rootnative/impulse/draguseDrag
@rootnative/impulse/panusePan
@rootnative/impulse/swipeuseSwipe
@rootnative/impulse/pinchusePinch
@rootnative/impulse/rotateuseRotate
@rootnative/impulse/composeuseGestures
@rootnative/impulse/rawuseRawGesture
@rootnative/impulse/gesture-handlerRNGH's own primitives, re-exported under their original names.

The entry re-exports GestureDetector, because every hook's result needs it. You don't need a second gesture import to get it.

The package ships ESM only

There is no CommonJS build, and that is on purpose. CommonJS with code splitting defeats the Reanimated Babel plugin and ships worklets that crash on a device. Metro handles ESM. A Node-side tool that requires CommonJS doesn't.

Set up Jest​

Impulse ships its own Jest wiring, so you need one line:

// jest.config.js
module.exports = {
preset: require.resolve('@rootnative/impulse/jest-preset'),
}

The preset layers on @react-native/jest-preset. It adds RNGH's native-module mocks and widens transformIgnorePatterns, so Jest transforms the ESM bundle of Impulse and RNGH. Neither passes the default React Native pattern.

You also need @react-native/jest-preset as a devDependency:

npm install --save-dev @react-native/jest-preset

React Native 0.86 moved its Jest preset into that package and declares it as an optional peer. No package manager installs an optional peer, so you install it yourself, at the version that matches your react-native.

Drive gestures in a test with RNGH's own helpers. Tag the gesture so the helper can find it:

const tap = useTap({ testId: 'card', onTap: select })
import { fireGestureHandler, getByGestureTestId } from 'react-native-gesture-handler/jest-utils'

fireGestureHandler(getByGestureTestId('card'))

In a monorepo​

Metro needs exactly one copy of five packages. Two copies of Reanimated or RNGH load two native modules, and the gestures of the second copy never fire.

Point Metro at one copy of each:

// metro.config.js
const singletons = [
'react',
'react-native',
'react-native-gesture-handler',
'react-native-reanimated',
'react-native-worklets',
]

config.resolver.extraNodeModules = singletons.reduce((acc, name) => {
acc[name] = path.resolve(workspaceRoot, 'node_modules', name)
return acc
}, {})

If you use pnpm, also set a hoisted layout. Metro runs with disableHierarchicalLookup, so an isolated node_modules tree breaks its resolver with Unable to resolve "invariant" from react-native/index.js.

# pnpm-workspace.yaml
nodeLinker: hoisted

Check that it works​

import { GestureDetector, useTap } from '@rootnative/impulse'

function Card() {
const tap = useTap({ onTap: () => console.log('tapped') })

return (
<GestureDetector gesture={tap.gesture}>
<View style={styles.card} />
</GestureDetector>
)
}

Tap the card. If the log doesn't appear, check the root view first.

Next: Composition relates gestures to one another, and Coexistence makes them share a touch with a scroll view.