Skip to content

Commit a0d7602

Browse files
authored
feat: Auto-detect expo-haptics when available (#558)
## Summary Adds an optional `expo-haptics` adapter so haptics work out of the box in any Expo app (including Expo Go), with automatic fallback to `react-native-haptic-feedback` for bare React Native. The adapter reads the native module from the Expo runtime registry (`globalThis.expo.modules.ExpoHaptics`) instead of importing `expo-haptics`, so it does not break Metro bundling for apps that do not have it installed. No new dependency is added to the library. ## Order Stacked on #554 — merge that first.
1 parent 5c84117 commit a0d7602

8 files changed

Lines changed: 70 additions & 15 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ React Native Sortables is a powerful and easy-to-use library that brings smooth,
4444

4545
- **Auto-scrolling** beyond screen bounds
4646
- Customizable **layout animations** for items addition and removal
47-
- Built-in **haptic feedback** integration (requires [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) dependency)
47+
- Built-in **haptic feedback** integration via [expo-haptics](https://docs.expo.dev/versions/latest/sdk/haptics/) or [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback)
4848
- Different **reordering strategies** (insertion, swapping)
4949

5050
- 💡 **Developer Experience**

packages/docs/docs/flex/props.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ Whether haptics are enabled. Vibrations are fired when the **pressed item become
899899

900900
:::important
901901

902-
To use built-in haptics, you have to install `react-native-haptic-feedback` package. See this [Getting Started](../getting-started#optional-dependencies) section for more details.
902+
To use built-in haptics, install `expo-haptics` (Expo apps, including Expo Go) or `react-native-haptic-feedback` (bare React Native). The library auto-detects whichever is available. See this [Getting Started](../getting-started#optional-dependencies) section for more details.
903903

904904
You can also use any other haptics library but you will have to trigger haptics manually when callbacks are called. See the [Callbacks](#callbacks) section for more details.
905905

packages/docs/docs/getting-started.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ Before getting started, you need to install and configure the following dependen
1919

2020
### Optional Dependencies
2121

22-
- **react-native-haptic-feedback**: For haptic feedback support
23-
- Follow the installation guide in the [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) README
22+
Haptic feedback is optional. The library automatically detects which haptics package is available, preferring **expo-haptics**:
23+
24+
- **expo-haptics**: recommended for Expo apps (already bundled in Expo Go). Install with `npx expo install expo-haptics`
25+
- **react-native-haptic-feedback**: for bare React Native apps. Follow the installation guide in its [README](https://github.com/mkuczera/react-native-haptic-feedback)
2426

2527
## 2. Installation
2628

packages/docs/docs/grid/props.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ Whether haptics are enabled. Vibrations are fired when the **pressed item become
785785

786786
:::important
787787

788-
To use built-in haptics, you have to install `react-native-haptic-feedback` package. See this [Getting Started](../getting-started#optional-dependencies) section for more details.
788+
To use built-in haptics, install `expo-haptics` (Expo apps, including Expo Go) or `react-native-haptic-feedback` (bare React Native). The library auto-detects whichever is available. See this [Getting Started](../getting-started#optional-dependencies) section for more details.
789789

790790
You can also use any other haptics library but you will have to trigger haptics manually when callbacks are called. See the [Callbacks](#callbacks) section for more details.
791791

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Optional expo-haptics adapter.
3+
*
4+
* We never import `expo-haptics` directly, because a static import would break
5+
* Metro bundling for bare React Native apps that don't have it installed.
6+
* Instead we read its native module from the Expo runtime registry, so it's
7+
* picked up automatically in any Expo app (including Expo Go) and ignored
8+
* everywhere else.
9+
*/
10+
11+
/* eslint-disable @typescript-eslint/no-explicit-any */
12+
/* eslint-disable @typescript-eslint/no-unsafe-call */
13+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
14+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
15+
16+
import { runOnJS } from 'react-native-reanimated';
17+
18+
const load = () => {
19+
const expoHaptics = (globalThis as any).expo?.modules?.ExpoHaptics;
20+
21+
if (!expoHaptics?.impactAsync) {
22+
return null;
23+
}
24+
25+
const impact = (style: string) => {
26+
try {
27+
// expo-haptics' native method is async; fire-and-forget and swallow
28+
// rejections (e.g. when haptics are unsupported on the device)
29+
const result = expoHaptics.impactAsync(style);
30+
result?.catch?.(() => {
31+
// ignore rejection
32+
});
33+
} catch {
34+
// ignore
35+
}
36+
};
37+
38+
const trigger = (type = 'impactLight') => {
39+
'worklet';
40+
// expo-haptics runs on the JS thread, so hop over from the UI worklet
41+
runOnJS(impact)(type === 'impactMedium' ? 'medium' : 'light');
42+
};
43+
44+
return trigger;
45+
};
46+
47+
const ExpoHaptics = { load };
48+
49+
export default ExpoHaptics;
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
export { default as ReactNativeHapticFeedback } from './react-native-haptic-feedback';
1+
import ExpoHaptics from './expo-haptics';
2+
import ReactNativeHapticFeedback from './react-native-haptic-feedback';
3+
4+
export const Haptics = {
5+
// Prefer expo-haptics (available in any Expo app, including Expo Go) and
6+
// fall back to react-native-haptic-feedback for bare React Native apps.
7+
load: () => ExpoHaptics.load() ?? ReactNativeHapticFeedback.load()
8+
};
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import type { HapticOptions } from 'react-native-haptic-feedback';
2-
3-
export const ReactNativeHapticFeedback = {
4-
load: () => (_type: string, _options?: HapticOptions) => {
5-
// noop
1+
export const Haptics = {
2+
load: () => (_type?: string) => {
3+
// noop on web
64
}
75
};

packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,21 @@ import { useCallback, useMemo } from 'react';
22
import { useDerivedValue } from 'react-native-reanimated';
33

44
import { IS_WEB } from '../../../constants';
5-
import { ReactNativeHapticFeedback } from '../adapters';
5+
import { Haptics } from '../adapters';
66

77
type HapticImpact = {
88
light(): void;
99
medium(): void;
1010
};
1111

12-
let hapticFeedback: null | ReturnType<typeof ReactNativeHapticFeedback.load> =
13-
null;
12+
let hapticFeedback: null | ReturnType<typeof Haptics.load> = null;
1413

1514
export default function useHaptics(enabled: boolean): HapticImpact {
1615
const isEnabled = !IS_WEB && enabled;
1716
const enabledValue = useDerivedValue(() => isEnabled);
1817

1918
if (isEnabled && !hapticFeedback) {
20-
hapticFeedback = ReactNativeHapticFeedback.load();
19+
hapticFeedback = Haptics.load();
2120
}
2221

2322
const light = useCallback(() => {

0 commit comments

Comments
 (0)