Installation
Platforms
Impulse runs where react-native-gesture-handler runs. This table shows what
we checked on each platform.
| Platform | Status |
|---|---|
| iOS | Supported (alpha). An XCUITest sweep on an iOS simulator drove every intent. No physical device ran it. |
| Android | Supported (alpha). An injected-touch sweep on an Android emulator drove every intent. No physical device ran it. |
| Web | Partial. 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, Windows | Not tested. |
| Electron | Not 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.
| Peer | Range |
|---|---|
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 |
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 },
})
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'
| Subpath | What it carries |
|---|---|
@rootnative/impulse | Every hook, the types, and GestureDetector. |
@rootnative/impulse/tap | useTap |
@rootnative/impulse/double-tap | useDoubleTap |
@rootnative/impulse/long-press | useLongPress |
@rootnative/impulse/drag | useDrag |
@rootnative/impulse/pan | usePan |
@rootnative/impulse/swipe | useSwipe |
@rootnative/impulse/pinch | usePinch |
@rootnative/impulse/rotate | useRotate |
@rootnative/impulse/compose | useGestures |
@rootnative/impulse/raw | useRawGesture |
@rootnative/impulse/gesture-handler | RNGH'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.
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.