Skip to content

Commit 37cc243

Browse files
committed
feat: Auto-detect expo-haptics when available
Add an optional expo-haptics adapter that reads the native module from the Expo runtime registry (globalThis.expo.modules.ExpoHaptics) instead of importing the package. This way it is picked up automatically in any Expo app (including Expo Go) without breaking Metro bundling for bare React Native apps that don't have it installed. Haptics now prefer expo-haptics and fall back to react-native-haptic-feedback for bare React Native setups.
1 parent d54d28a commit 37cc243

4 files changed

Lines changed: 63 additions & 10 deletions

File tree

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)