@usefy/hooks
v0.2.5
Published
A collection of useful React hooks
Maintainers
Readme
⚠️ Pre-release Notice: This project is currently in version
0.x.x(alpha/beta stage). APIs may change between minor versions. While fully functional and tested, please use with caution in production environments.🚧 Actively Developing: New hooks are being added regularly. Stay tuned for more utilities!
Overview
usefy is a collection of production-ready custom hooks designed for modern React applications. All hooks are written in TypeScript, providing complete type safety, comprehensive testing, and minimal bundle size.
✨ Why usefy?
- 🚀 Zero Dependencies — Pure React implementation with no external dependencies
- 📦 Tree Shakeable — Import only the hooks you need to optimize bundle size
- 🔷 TypeScript First — Complete type safety with full autocomplete support
- ⚡ SSR Compatible — Works seamlessly with Next.js, Remix, and other SSR frameworks
- 🧪 Well Tested — High test coverage ensures reliability and stability
- 📖 Well Documented — Detailed documentation with practical examples
- 🎨 Interactive Demos — Try all hooks in action with our Storybook playground
Installation
All-in-One Package
Install all hooks at once:
# npm
npm install @usefy/hooks
# yarn
yarn add @usefy/hooks
# pnpm
pnpm add @usefy/hooksIndividual Packages
You can also install only the hooks you need:
# Example: Install only use-toggle
pnpm add @usefy/use-toggle
# Install multiple packages
pnpm add @usefy/use-debounce @usefy/use-local-storagePeer Dependencies
All packages require React 18 or 19:
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}Packages
📦 Available Hooks
| Hook | Description | npm | Coverage |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| @usefy/use-toggle | Boolean state management with toggle, setTrue, setFalse | | |
| @usefy/use-counter | Counter state with increment, decrement, reset | |
|
| @usefy/use-debounce | Value debouncing with leading/trailing edge | |
|
| @usefy/use-debounce-callback | Debounced callbacks with cancel/flush/pending | |
|
| @usefy/use-throttle | Value throttling for rate-limiting updates | |
|
| @usefy/use-throttle-callback | Throttled callbacks with cancel/flush/pending | |
|
| @usefy/use-local-storage | localStorage persistence with cross-tab sync | |
|
| @usefy/use-session-storage | sessionStorage persistence for tab lifetime | |
|
| @usefy/use-click-any-where | Document-wide click event detection | |
|
| @usefy/use-copy-to-clipboard | Clipboard copy with fallback support | |
|
| @usefy/use-event-listener | DOM event listener with auto cleanup | |
|
| @usefy/use-on-click-outside | Outside click detection for modals/dropdowns | |
|
| @usefy/use-unmount | Execute callback on component unmount | |
|
| @usefy/use-init | One-time initialization with async, retry, timeout | |
|
| @usefy/use-timer | Countdown timer with drift compensation and formats | |
|
| @usefy/use-geolocation | Device geolocation with real-time tracking and distance | |
|
| @usefy/use-intersection-observer | Element visibility detection with Intersection Observer | |
|
| @usefy/use-signal | Event-driven communication between components | |
|
| @usefy/use-memory-monitor | Real-time browser memory monitoring with leak detection | |
|
Quick Start
Using the All-in-One Package
import {
useToggle,
useCounter,
useDebounce,
useLocalStorage,
useCopyToClipboard,
useEventListener,
useOnClickOutside,
useIntersectionObserver,
useSignal,
useUnmount,
useInit,
} from "@usefy/hooks";
function App() {
// Boolean state management
const { value: isOpen, toggle, setFalse: close } = useToggle(false);
// Counter with controls
const { count, increment, decrement, reset } = useCounter(0);
// Debounced search
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
// Persistent theme preference
const [theme, setTheme] = useLocalStorage("theme", "light");
// Copy functionality
const [copiedText, copy] = useCopyToClipboard();
// Lazy loading image
const { ref: imageRef, inView } = useIntersectionObserver({
triggerOnce: true,
rootMargin: "50px",
});
return (
<div data-theme={theme}>
{/* Modal */}
<button onClick={toggle}>Open Modal</button>
{isOpen && (
<div className="modal">
<button onClick={close}>Close</button>
</div>
)}
{/* Counter */}
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
{/* Search */}
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{/* Theme Toggle */}
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
{/* Copy */}
<button onClick={() => copy("Hello World!")}>
{copiedText ? "Copied!" : "Copy"}
</button>
{/* Lazy Loading */}
<div ref={imageRef}>
{inView && <img src="large-image.jpg" alt="Lazy loaded" />}
</div>
</div>
);
}Using Individual Packages
import { useToggle } from "@usefy/use-toggle";
import { useDebounce } from "@usefy/use-debounce";
function SearchModal() {
const { value: isOpen, toggle } = useToggle(false);
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return (
<>
<button onClick={toggle}>Search</button>
{isOpen && (
<input value={query} onChange={(e) => setQuery(e.target.value)} />
)}
</>
);
}Features
🔄 State Management
const { value, toggle, setTrue, setFalse, setValue } = useToggle(false);Perfect for modals, dropdowns, accordions, and switches.
const { count, increment, decrement, reset } = useCounter(0);Ideal for quantity selectors, pagination, and score tracking.
⏱️ Timing Utilities
const debouncedValue = useDebounce(value, 300, {
leading: false,
trailing: true,
maxWait: 1000,
});Best for search inputs, form validation, and API calls.
const debouncedFn = useDebounceCallback(callback, 300);
debouncedFn(args); // Call debounced
debouncedFn.cancel(); // Cancel pending
debouncedFn.flush(); // Execute immediately
debouncedFn.pending(); // Check if pendingconst throttledValue = useThrottle(value, 100, {
leading: true,
trailing: true,
});Perfect for scroll events, resize handlers, and mouse tracking.
const throttledFn = useThrottleCallback(callback, 100);import { useTimer, ms } from "@usefy/use-timer";
const timer = useTimer(ms.minutes(5), {
format: "MM:SS",
autoStart: false,
loop: false,
onComplete: () => console.log("Time's up!"),
});
// Controls
timer.start();
timer.pause();
timer.reset();
timer.addTime(ms.seconds(10));
timer.subtractTime(ms.seconds(5));
// State
timer.time; // "05:00"
timer.progress; // 0-100
timer.isRunning; // booleanPerfect for countdown timers, Pomodoro apps, kitchen timers, and time-based UIs with smart render optimization.
💾 Storage
const [value, setValue, removeValue] = useLocalStorage("key", initialValue, {
serializer: JSON.stringify,
deserializer: JSON.parse,
syncTabs: true,
onError: (error) => console.error(error),
});Supports cross-tab synchronization and custom serialization.
const [value, setValue, removeValue] = useSessionStorage("key", initialValue);Data persists during tab lifetime, isolated per tab.
📡 Communication
import { useSignal } from "@usefy/use-signal";
// Emitter component
function RefreshButton() {
const { emit, info } = useSignal("dashboard-refresh");
return (
<button onClick={() => emit()}>
Refresh All ({info.subscriberCount} widgets)
</button>
);
}
// Subscriber component
function DataWidget() {
const { signal } = useSignal("dashboard-refresh");
useEffect(() => {
fetchData(); // Refetch when signal changes
}, [signal]);
return <div>Widget Content</div>;
}
// With typed data payload
interface NotificationData {
type: "success" | "error";
message: string;
}
function NotificationEmitter() {
const { emit } = useSignal<NotificationData>("notification");
return (
<button onClick={() => emit({ type: "success", message: "Done!" })}>
Notify
</button>
);
}
function NotificationReceiver() {
const { signal, info } = useSignal<NotificationData>("notification");
useEffect(() => {
if (signal > 0 && info.data) {
toast[info.data.type](info.data.message);
}
}, [signal]);
return null;
}Perfect for: Dashboard refresh, form reset, cache invalidation, multi-step flows, and event broadcasting.
⚠️ Note:
useSignalis NOT a global state management solution. It's designed for lightweight event-driven communication. For complex state management, use Context, Zustand, Jotai, or Recoil.
🖱️ Events
// Window resize event (default target)
useEventListener("resize", (e) => {
console.log("Window resized:", window.innerWidth);
});
// Document keydown event
useEventListener(
"keydown",
(e) => {
if (e.key === "Escape") closeModal();
},
document
);
// Element with ref
const buttonRef = useRef<HTMLButtonElement>(null);
useEventListener("click", handleClick, buttonRef);
// With options
useEventListener("scroll", handleScroll, window, {
passive: true,
capture: false,
enabled: isTracking,
});Supports window, document, HTMLElement, and RefObject targets with full TypeScript type inference.
// Basic usage - close modal on outside click
const modalRef = useRef<HTMLDivElement>(null);
useOnClickOutside(modalRef, () => onClose(), { enabled: isOpen });
// Multiple refs - button and dropdown menu
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
useOnClickOutside([buttonRef, menuRef], () => setIsOpen(false), {
enabled: isOpen,
});
// With exclude refs
useOnClickOutside(modalRef, onClose, {
excludeRefs: [toastRef], // Clicks on toast won't close modal
});Perfect for modals, dropdowns, popovers, tooltips, and context menus with mouse + touch support.
useClickAnyWhere(
(event) => {
if (!ref.current?.contains(event.target)) {
closeMenu();
}
},
{ enabled: isOpen }
);Ideal for closing dropdowns, modals, and context menus.
const [copiedText, copy] = useCopyToClipboard({
timeout: 2000,
onSuccess: (text) => toast.success("Copied!"),
onError: (error) => toast.error("Failed to copy"),
});
const success = await copy("text to copy");Modern Clipboard API with automatic fallback for older browsers.
📍 Location
import { useGeolocation } from "@usefy/use-geolocation";
// Basic usage - get current position
const { position, loading, error } = useGeolocation();
// Real-time tracking
const { position, watchPosition, clearWatch } = useGeolocation({
immediate: false,
watch: false,
onPositionChange: (pos) => console.log("Position updated:", pos),
});
// Distance calculation
const { position, distanceFrom, bearingTo } = useGeolocation();
// Calculate distance to New York (in meters)
const distance = distanceFrom(40.7128, -74.006);
// Calculate bearing/direction to London (0-360 degrees)
const bearing = bearingTo(51.5074, -0.1278);
// High accuracy mode
const { position } = useGeolocation({
enableHighAccuracy: true,
timeout: 10000,
});
// Permission tracking
const { permission } = useGeolocation({
onPermissionChange: (state) => {
console.log("Permission:", state); // "prompt" | "granted" | "denied" | "unavailable"
},
});Perfect for location-based apps, maps, navigation, distance tracking, and geofencing with built-in Haversine distance calculation and bearing utilities.
👁️ Visibility
import { useIntersectionObserver } from "@usefy/use-intersection-observer";
// Basic usage - detect when element enters viewport
const { ref, inView, entry } = useIntersectionObserver();
// Lazy loading images
const { ref, inView } = useIntersectionObserver({
triggerOnce: true, // Stop observing after first detection
threshold: 0.1, // Trigger when 10% visible
rootMargin: "50px", // Start loading 50px before entering viewport
});
// Infinite scroll with sentinel element
const { ref, inView } = useIntersectionObserver({
threshold: 1.0,
rootMargin: "100px", // Preload 100px ahead
});
useEffect(() => {
if (inView) loadMoreItems();
}, [inView]);
// Scroll animations
const { ref, inView } = useIntersectionObserver({
triggerOnce: true,
threshold: 0.3,
});
// Progress tracking with multiple thresholds
const thresholds = Array.from({ length: 101 }, (_, i) => i / 100);
const { ref, entry } = useIntersectionObserver({
threshold: thresholds,
onChange: (entry) => {
setProgress(Math.round(entry.intersectionRatio * 100));
},
});
// Custom scroll container
const containerRef = useRef<HTMLDivElement>(null);
const { ref, inView } = useIntersectionObserver({
root: containerRef.current,
rootMargin: "0px",
});
// Delayed observation
const { ref, inView } = useIntersectionObserver({
delay: 500, // Wait 500ms before creating observer
});Perfect for lazy loading, infinite scroll, scroll animations, progress tracking, and any visibility-based interactions with smart re-render optimization.
🔄 Lifecycle
// Basic usage
useUnmount(() => {
console.log("Component unmounted");
});
// With latest state access
const [formData, setFormData] = useState({});
useUnmount(() => {
// Always accesses latest formData value
saveToLocalStorage(formData);
});
// Conditional cleanup
useUnmount(
() => {
sendAnalyticsEvent("component_unmounted");
},
{ enabled: trackingEnabled }
);Perfect for saving data, sending analytics, and cleaning up resources on component removal.
// Basic async initialization
const { isInitialized, isInitializing, error } = useInit(async () => {
await loadConfiguration();
});
// With retry and timeout
const { error, reinitialize } = useInit(
async () => {
await connectToServer();
},
{
retry: 3,
retryDelay: 1000,
timeout: 5000,
}
);
// Conditional initialization
useInit(
() => {
initializeFeature();
},
{ when: isEnabled }
);
// With cleanup function
useInit(() => {
const subscription = eventBus.subscribe();
return () => subscription.unsubscribe();
});Perfect for initializing services, loading configuration, setting up subscriptions, and any one-time setup tasks with robust error handling.
Test Coverage
All packages are comprehensively tested using Vitest to ensure reliability and stability.
📊 View Detailed Coverage Report (GitHub Pages)
💡 To generate coverage report locally, run
pnpm test:coverage. The report will be available atcoverage/index.html.
| Package | Statements | Branches | Functions | Lines | | ------------------------- | ---------- | -------- | --------- | ------ | | use-toggle | 100% | 100% | 100% | 100% | | use-counter | 100% | 100% | 100% | 100% | | use-throttle | 100% | 100% | 100% | 100% | | use-throttle-callback | 100% | 100% | 100% | 100% | | use-on-click-outside | 97.61% | 93.93% | 100% | 97.61% | | use-event-listener | 96.29% | 91.66% | 100% | 96.29% | | use-init | 96.1% | 88.63% | 100% | 96% | | use-local-storage | 95.18% | 86.84% | 93.75% | 95.12% | | use-session-storage | 94.66% | 82.75% | 93.33% | 94.59% | | use-debounce-callback | 93.2% | 76% | 93.75% | 93.13% | | use-click-any-where | 92.3% | 87.5% | 100% | 92.3% | | use-debounce | 90% | 82.6% | 66.66% | 91.95% | | use-copy-to-clipboard | 87.87% | 79.16% | 85.71% | 87.87% | | use-unmount | 100% | 100% | 100% | 100% | | use-timer | 83.8% | 72.63% | 93.93% | 84.13% | | use-geolocation | 93.89% | 93.47% | 100% | 93.75% | | use-intersection-observer | 94% | 85% | 95% | 93.93% | | use-signal | 98.61% | 90.9% | 96.42% | 98.59% | | use-memory-monitor | 89.05% | 71.64% | 87.87% | 92.27% |
Browser Support
| Browser | Version | | ------- | ---------------- | | Chrome | 66+ | | Firefox | 63+ | | Safari | 13.1+ | | Edge | 79+ | | IE 11 | Fallback support |
Related Links
- 📦 npm Organization
- 🐙 GitHub Repository
- 📝 Changelog
- 🐛 Issue Tracker
License
MIT © mirunamu
