A fully type-safe React Native & Expo package to gracefully prevent double taps, rapid presses, and manage async button states. Supports lock modes, cooldowns, progress tracking, and optional built-in haptic feedback.
Zero dependencies*. (*Optionally integrates dynamically with expo-haptics).
npm install react-native-press-guard
# or
yarn add react-native-press-guard- Double-tap prevention: Automatically locks your button until the async
onPresspromise resolves. - Cooldown mode: Prevent rapid tapping by setting a custom millisecond cooldown.
- Hybrid mode: Wait for the promise to resolve, then enforce the cooldown.
- Drop-in Component: Use
<PressGuard>exactly like a React Native<Pressable>. - Custom Hook: Build your own guarded components using
usePressGuard. - HOC Wrapper: Wrap existing components with
withPressGuard(YourComponent). - Render Props: Exposes
isLocked,progress, andunlockstates. - Optional Haptics: Automatically uses
expo-haptics(if installed in your app) for rich feedback on press, success, and error events.
<PressGuard> is a drop-in replacement for the standard <Pressable>.
import { PressGuard } from "react-native-press-guard";
import { Text } from "react-native";
export default function App() {
const submitData = async () => {
await fetch("https://api.example.com/submit", { method: "POST" });
};
return (
<PressGuard
onPress={submitData}
mode="promise"
haptics={{ press: "Light", success: "Success" }}
style={({ pressed }) => [{ opacity: pressed ? 0.7 : 1 }]}
>
{({ isLocked }) => <Text>{isLocked ? "Submitting..." : "Submit"}</Text>}
</PressGuard>
);
}If you want more control, use the hook directly inside your own custom button.
import React from "react";
import { TouchableOpacity, Text, View } from "react-native";
import { usePressGuard } from "react-native-press-guard";
export const MySafeButton = ({ onPress }) => {
const {
onPress: guardedPress,
isLocked,
progress,
} = usePressGuard(onPress, {
mode: "cooldown",
cooldown: 2000,
haptics: { press: "Medium" },
});
return (
<TouchableOpacity onPress={guardedPress} disabled={isLocked}>
<Text>Tap Me</Text>
{isLocked && (
<View
style={{
height: 4,
width: `${progress * 100}%`,
backgroundColor: "red",
}}
/>
)}
</TouchableOpacity>
);
};You can wrap any component that accepts an onPress and disabled prop.
import { Button } from "react-native";
import { withPressGuard } from "react-native-press-guard";
// Now it's perfectly safe!
const SafeButton = withPressGuard(Button, { mode: "promise" });
export default () => (
<SafeButton title="Press Me" onPress={async () => await doWork()} />
);When using cooldown or hybrid mode, usePressGuard (and <PressGuard>) exposes a progress value between 0 and 1. This uses requestAnimationFrame to give you butter-smooth updates for building cooldown pie-charts, loaders, or progress bars.
<PressGuard onPress={syncAction} mode="cooldown" cooldown={3000}>
{({ isLocked, progress }) => (
<View style={{ opacity: isLocked ? 0.5 : 1 }}>
<Text>Send Message</Text>
{isLocked && <Text>Wait {Math.round((1 - progress) * 3)}s</Text>}
</View>
)}
</PressGuard>If your app has expo-haptics installed (npx expo install expo-haptics), you can trigger haptic feedback automatically at different lifecycle stages.
Options available:
'Light' | 'Medium' | 'Heavy' | 'Success' | 'Warning' | 'Error'
<PressGuard
onPress={asyncAction}
haptics={{
press: "Medium", // Triggered immediately when pressed
success: "Success", // Triggered when promise resolves
error: "Error", // Triggered if promise rejects
cooldownStart: "Light", // Triggered when cooldown phase begins
cooldownEnd: "Light", // Triggered when button is unlocked
}}
>
<Text>Haptic Button</Text>
</PressGuard>| Property | Type | Default | Description |
|---|---|---|---|
mode |
'promise' | 'cooldown' | 'hybrid' |
'promise' |
Behavior mode. |
cooldown |
number |
1000 |
Cooldown duration in ms. Native to cooldown and hybrid. |
disabled |
boolean |
false |
Disable the press guard completely. |
haptics |
object |
{} |
Haptic feedback configuration object map. |
onSuccess |
(res: T) => void |
undefined |
Callback fired when promise successfully resolves. |
onError |
(err: Error) => void |
undefined |
Callback fired when promise rejects or handler throws. |
promise: Unlocks automatically when the returnedPromiseresolves or rejects.cooldown: Regardless of standard functions or promises, locks strictly for thecooldownms duration after the tap.hybrid: Wait for the promise to resolve, but wait at least thecooldownduration before unlocking.
MIT