From 37cc243dce5fd04d5888f8b9828a0c0f0c86f6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Sun, 7 Jun 2026 16:28:10 +0200 Subject: [PATCH 1/2] 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. --- .../haptics/adapters/expo-haptics.ts | 49 +++++++++++++++++++ .../haptics/adapters/index.native.ts | 9 +++- .../integrations/haptics/adapters/index.ts | 8 ++- .../integrations/haptics/hooks/useHaptics.ts | 7 ++- 4 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 packages/react-native-sortables/src/integrations/haptics/adapters/expo-haptics.ts diff --git a/packages/react-native-sortables/src/integrations/haptics/adapters/expo-haptics.ts b/packages/react-native-sortables/src/integrations/haptics/adapters/expo-haptics.ts new file mode 100644 index 00000000..5288e0be --- /dev/null +++ b/packages/react-native-sortables/src/integrations/haptics/adapters/expo-haptics.ts @@ -0,0 +1,49 @@ +/** + * Optional expo-haptics adapter. + * + * We never import `expo-haptics` directly, because a static import would break + * Metro bundling for bare React Native apps that don't have it installed. + * Instead we read its native module from the Expo runtime registry, so it's + * picked up automatically in any Expo app (including Expo Go) and ignored + * everywhere else. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { runOnJS } from 'react-native-reanimated'; + +const load = () => { + const expoHaptics = (globalThis as any).expo?.modules?.ExpoHaptics; + + if (!expoHaptics?.impactAsync) { + return null; + } + + const impact = (style: string) => { + try { + // expo-haptics' native method is async; fire-and-forget and swallow + // rejections (e.g. when haptics are unsupported on the device) + const result = expoHaptics.impactAsync(style); + result?.catch?.(() => { + // ignore rejection + }); + } catch { + // ignore + } + }; + + const trigger = (type = 'impactLight') => { + 'worklet'; + // expo-haptics runs on the JS thread, so hop over from the UI worklet + runOnJS(impact)(type === 'impactMedium' ? 'medium' : 'light'); + }; + + return trigger; +}; + +const ExpoHaptics = { load }; + +export default ExpoHaptics; diff --git a/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts b/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts index df59ef65..e60bc4ab 100644 --- a/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts +++ b/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts @@ -1 +1,8 @@ -export { default as ReactNativeHapticFeedback } from './react-native-haptic-feedback'; +import ExpoHaptics from './expo-haptics'; +import ReactNativeHapticFeedback from './react-native-haptic-feedback'; + +export const Haptics = { + // Prefer expo-haptics (available in any Expo app, including Expo Go) and + // fall back to react-native-haptic-feedback for bare React Native apps. + load: () => ExpoHaptics.load() ?? ReactNativeHapticFeedback.load() +}; diff --git a/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts b/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts index f039c3b9..2f367f58 100644 --- a/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts +++ b/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts @@ -1,7 +1,5 @@ -import type { HapticOptions } from 'react-native-haptic-feedback'; - -export const ReactNativeHapticFeedback = { - load: () => (_type: string, _options?: HapticOptions) => { - // noop +export const Haptics = { + load: () => (_type?: string) => { + // noop on web } }; diff --git a/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts b/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts index 61aa64e2..a90cabdd 100644 --- a/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts +++ b/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts @@ -2,22 +2,21 @@ import { useCallback, useMemo } from 'react'; import { useDerivedValue } from 'react-native-reanimated'; import { IS_WEB } from '../../../constants'; -import { ReactNativeHapticFeedback } from '../adapters'; +import { Haptics } from '../adapters'; type HapticImpact = { light(): void; medium(): void; }; -let hapticFeedback: null | ReturnType = - null; +let hapticFeedback: null | ReturnType = null; export default function useHaptics(enabled: boolean): HapticImpact { const isEnabled = !IS_WEB && enabled; const enabledValue = useDerivedValue(() => isEnabled); if (isEnabled && !hapticFeedback) { - hapticFeedback = ReactNativeHapticFeedback.load(); + hapticFeedback = Haptics.load(); } const light = useCallback(() => { From 76c290305161898e3227329fb64e8baa3d05c4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Sun, 7 Jun 2026 16:50:03 +0200 Subject: [PATCH 2/2] docs: Mention expo-haptics in haptics setup docs --- README.md | 2 +- packages/docs/docs/flex/props.mdx | 2 +- packages/docs/docs/getting-started.mdx | 6 ++++-- packages/docs/docs/grid/props.mdx | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3d7ce486..bda7a8bd 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ React Native Sortables is a powerful and easy-to-use library that brings smooth, - **Auto-scrolling** beyond screen bounds - Customizable **layout animations** for items addition and removal - - Built-in **haptic feedback** integration (requires [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) dependency) + - 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) - Different **reordering strategies** (insertion, swapping) - 💡 **Developer Experience** diff --git a/packages/docs/docs/flex/props.mdx b/packages/docs/docs/flex/props.mdx index 6ebe0ae0..d2b6f43b 100644 --- a/packages/docs/docs/flex/props.mdx +++ b/packages/docs/docs/flex/props.mdx @@ -899,7 +899,7 @@ Whether haptics are enabled. Vibrations are fired when the **pressed item become :::important -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. +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. 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. diff --git a/packages/docs/docs/getting-started.mdx b/packages/docs/docs/getting-started.mdx index 756158a9..28cdd1c2 100644 --- a/packages/docs/docs/getting-started.mdx +++ b/packages/docs/docs/getting-started.mdx @@ -19,8 +19,10 @@ Before getting started, you need to install and configure the following dependen ### Optional Dependencies -- **react-native-haptic-feedback**: For haptic feedback support - - Follow the installation guide in the [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) README +Haptic feedback is optional. The library automatically detects which haptics package is available, preferring **expo-haptics**: + +- **expo-haptics**: recommended for Expo apps (already bundled in Expo Go). Install with `npx expo install expo-haptics` +- **react-native-haptic-feedback**: for bare React Native apps. Follow the installation guide in its [README](https://github.com/mkuczera/react-native-haptic-feedback) ## 2. Installation diff --git a/packages/docs/docs/grid/props.mdx b/packages/docs/docs/grid/props.mdx index 2fb884dd..525d6c7c 100644 --- a/packages/docs/docs/grid/props.mdx +++ b/packages/docs/docs/grid/props.mdx @@ -785,7 +785,7 @@ Whether haptics are enabled. Vibrations are fired when the **pressed item become :::important -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. +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. 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.