@usefy/hooks
v1.1.0
Published
A collection of useful React hooks
Maintainers
Readme
✅ Stable & production-ready —
@usefy/hooksfollows semantic versioning; breaking changes only ever land in a new major version.🚧 Actively developing: new hooks are added regularly, and the umbrella re-exports every one.
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-cookie | Browser cookie as React state, SSR-aware | |
|
| @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 | |
|
| @usefy/use-hover | Element hover detection with delay support | |
|
| @usefy/use-key-press | Keyboard key, shortcut, and combination detection | |
|
| @usefy/use-map | Map data structure state with immutable updates | |
|
| @usefy/use-set | Set data structure state with immutable updates | |
|
| @usefy/use-list | Array state with push/filter/sort/insertAt/updateAt | |
|
| @usefy/use-queue | FIFO queue state with enqueue/dequeue and immutable updates | |
|
| @usefy/use-history-state | Undo/redo state history with time-travel | |
|
| @usefy/use-step | Multi-step navigation for wizards, forms, carousels | |
|
| @usefy/use-window-size | Window size tracking with debounce/throttle and SSR | |
|
| @usefy/use-is-client | True once hydrated on the client (SSR guard) | |
|
| @usefy/use-isomorphic-layout-effect | SSR-safe useLayoutEffect | |
|
| @usefy/use-previous | Previous distinct value (last render it changed) | |
|
| @usefy/use-latest | Ref that always holds the latest value | |
|
| @usefy/use-event-callback | Stable callback that sees the latest state | |
|
| @usefy/use-update-effect | useEffect that skips the first render | |
|
| @usefy/use-mount | Run a callback once on mount | |
|
| @usefy/use-is-first-render | True only on the first render | |
|
| @usefy/use-media-query | Match CSS media queries (matchMedia, SSR-safe) | |
|
| @usefy/use-preferred-color-scheme | System color scheme (prefers-color-scheme) | |
|
| @usefy/use-reduced-motion | Reduced-motion preference (a11y) | |
|
| @usefy/use-dark-mode | Dark mode: system/light/dark, persistence, DOM apply | |
|
| @usefy/use-document-title | Set document.title with restore-on-unmount | |
|
| @usefy/use-controllable-state | Controlled/uncontrolled state primitive (Radix/Mantine pattern) | |
|
| @usefy/use-merged-refs | Merge multiple refs into one (forwardRef helper) | |
|
| @usefy/use-disclosure | open/close/toggle state for modals, drawers, popovers | |
|
| @usefy/use-measure | Reactive element bounds (x, y, width, height, top, right, bottom, left) via ResizeObserver | |
|
| @usefy/use-mutation-observer | Watch an element for DOM mutations (childList/attributes/characterData) via MutationObserver | |
|
| @usefy/use-scroll-position | Throttled scroll offset (x, y) of the window or an element | |
|
| @usefy/use-scroll-lock | Lock body scroll for modals/drawers — iOS-aware, nested-lock counted, with scroll-position restore | |
|
| @usefy/use-hotkeys | High-level keyboard shortcuts — combos, sequences,
mod alias, scoping, input-field guard | | |
| @usefy/use-focus-trap | Trap keyboard focus inside a subtree (modals/dialogs) — Tab cycling, initial focus, restore on close | |
|
| @usefy/use-focus-within | Track whether keyboard focus is anywhere within a subtree — reactive
:focus-within with onFocus/onBlur edges | | |
| @usefy/use-long-press | Long-press ("press and hold") gestures for mouse and touch — time threshold, movement cancellation,
onStart/onFinish/onCancel | | |
| @usefy/use-async-fn | Run a manual-trigger async function with idle/pending/success/error lifecycle, race-safe stale-response guarding, and unmount safety | |
|
| @usefy/use-async | Full async task lifecycle — object-style state, immediate auto-run, and AbortController cancellation | |
|
| @usefy/use-polling | Poll an async function on an interval — non-overlapping ticks, pause/resume, an
enabled gate, and exponential backoff | | |
| @usefy/use-raf-state | A drop-in
useState that batches updates to requestAnimationFrame — rapid scroll/resize/pointer/animation updates coalesce to at most one commit per frame | | |
| @usefy/use-object-state | Object state with immutable partial updates (patch/merge) and reset | |
|
| @usefy/use-stack | LIFO stack state with push/pop/peek and immutable updates | |
|
| @usefy/use-selection | Multi/single selection state for lists and tables — Set-based, checkbox-ready with indeterminate support | |
|
| @usefy/use-infinite-scroll | Sentinel-driven infinite loading built on IntersectionObserver — fires
loadMore once per intersection, respects hasMore/loading/enabled | | |
| @usefy/use-pagination | Headless pagination state machine — controlled/uncontrolled current page, derived
pageCount, a slice-ready 0-based range, and an ellipsis-aware items pager model | | |
| @usefy/use-network-state | Online/offline status + Network Information API (
effectiveType, downlink, saveData), SSR-safe via useSyncExternalStore | | |
| @usefy/use-page-visibility | Track tab/window visibility (foreground vs. background) via the Page Visibility API, with optional
onChange and SSR support | | |
| @usefy/use-idle | Report user inactivity after a timeout, with throttled activity listeners, visibility awareness, and SSR support | |
|
| @usefy/use-permission | Read Permissions API status with live updates —
{ state, status, isSupported, error }, keyed on descriptor contents, SSR-safe, accepts any permission name | | |
| @usefy/use-script | Load an external script with
idle/loading/ready/error status, <script> deduplication across components, and ref-counted cleanup — SSR-safe and StrictMode-safe | | |
| @usefy/use-user-media | Camera and microphone streams —
getUserMedia lifecycle, device enumeration and switching, torch, normalized errors, and a teardown guarantee (no track outlives the component) | | |
Quick Start
Using the All-in-One Package
import {
useToggle,
useCounter,
useDebounce,
useLocalStorage,
useCookie,
useCopyToClipboard,
useEventListener,
useOnClickOutside,
useIntersectionObserver,
useHover,
useKeyPress,
useMap,
useSet,
useList,
useQueue,
useHistoryState,
useStep,
useWindowSize,
useNetworkState,
usePageVisibility,
useIdle,
usePermission,
useScript,
useSignal,
useUnmount,
useInit,
useIsClient,
useIsomorphicLayoutEffect,
usePrevious,
useLatest,
useEventCallback,
useUpdateEffect,
useMount,
useIsFirstRender,
useMediaQuery,
usePreferredColorScheme,
useReducedMotion,
useDarkMode,
useDocumentTitle,
useControllableState,
useMergedRefs,
useDisclosure,
useMeasure,
useMutationObserver,
useScrollPosition,
useScrollLock,
useHotkeys,
useFocusTrap,
useFocusWithin,
useLongPress,
useAsyncFn,
useAsync,
usePolling,
useRafState,
useObjectState,
useStack,
useSelection,
useInfiniteScroll,
usePagination,
} 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.
import { useMap } from "@usefy/use-map";
const [users, { set, setAll, remove, reset, clear, get }] = useMap<string, User>([
["1", { id: "1", name: "Alice" }],
]);
set("2", { id: "2", name: "Bob" }); // add / overwrite
remove("1"); // delete a key
setAll(entries); // replace everything
reset(); // back to initialImmutable updates (new Map on every change), a ReadonlyMap return type, stable action identities, lazy initialization, and no-op skipping. Perfect for keyed collections, caches, form field maps, and feature flags.
import { useSet } from "@usefy/use-set";
const [selected, { add, remove, toggle, has, clear, reset }] = useSet<string>(["1"]);
toggle("2"); // flip membership
toggle("3", true); // force add (like DOMTokenList.toggle)
has("1"); // membership check
remove("1"); // deleteImmutable updates (new Set on every change), a ReadonlySet return type, toggle with an optional force argument, stable action identities, lazy initialization, and no-op skipping. Perfect for multi-select, tag filters, and tracking selected ids.
import { useList } from "@usefy/use-list";
const [todos, { push, removeAt, updateAt, sort, filter, reset }] = useList<Todo>([]);
push({ id: 1, text: "Hi", completed: false }); // append
updateAt(0, { ...todos[0], completed: true }); // replace at index
removeAt(0); // delete at index
sort((a, b) => a.id - b.id); // immutable sortImmutable updates (new array on every change), a readonly T[] return type, a rich action set (set/push/filter/sort/clear/removeAt/insertAt/updateAt/reset), set with an updater function, stable action identities, lazy init, and no-op skipping. Perfect for todo lists, editable tables, and ordered collections.
import { useQueue } from "@usefy/use-queue";
const [queue, { add, remove, peek, clear, reset }] = useQueue<Task>([]);
add(task); // enqueue to the back (variadic: add(a, b, c))
const next = remove(); // dequeue the front and return it (undefined if empty)
peek(); // read the front without mutatingFIFO semantics (add to the back, remove/peek from the front), immutable updates (new array on every change), a readonly T[] return type, remove that returns the dequeued item, stable action identities, lazy init, and no-op skipping. Read first/last/size directly from the queue (queue[0], queue[queue.length - 1], queue.length). Perfect for task runners, print/job queues, message buffers, and breadth-first traversals.
import { useHistoryState } from "@usefy/use-history-state";
const { state, set, undo, redo, canUndo, canRedo, goTo, history } =
useHistoryState(initialCanvas, { limit: 50 });
set((s) => ({ ...s, dirty: true })); // record a new entry (value or updater)
undo(); // step back
redo(); // step forward
goTo(0); // jump to any point on the timelineUndo/redo/goTo time travel over an immutable timeline, set with value-or-updater (like useState), canUndo/canRedo flags, the full history array + currentIndex, an optional limit to bound memory, clear/reset, stable control identities (safe as effect deps), and no-op skipping. Perfect for editors, drawing/design canvases, form builders, and any Ctrl/Cmd+Z experience.
import { useStep } from "@usefy/use-step";
const [step, { goToNextStep, goToPrevStep, canGoToNextStep, canGoToPrevStep, setStep, reset }] =
useStep(4); // steps 0..3
goToNextStep(); // advance (no-op on the last step)
setStep(2); // jump to a step (value or updater, clamped)
setStep((s) => s + 1);0-based indexing with automatic range validation, goToNextStep/goToPrevStep plus ready-made canGoToNextStep/canGoToPrevStep flags for disabling buttons, setStep (value or updater), reset, stable control identities (safe as effect deps), resilience to a changing step count, and no-op skipping at the edges. Perfect for multi-step forms, wizards, onboarding flows, and carousels.
import { useObjectState } from "@usefy/use-object-state";
const [form, patch, reset] = useObjectState({
name: "",
email: "",
subscribe: false,
});
patch({ name: "Alice" }); // shallow-merge a partial (immutably)
patch((prev) => ({ subscribe: !prev.subscribe })); // functional updater form
reset(); // back to the initial object
reset({ name: "Bob", email: "", subscribe: true }); // reset to a provided objectA useState-style [state, patch, reset] tuple that sits between useState (replace the whole value) and useReducer (write a reducer). patch shallow-merges a Partial<T> immutably ({ ...prev, ...partial }) — update one field without spreading the whole object — or takes a functional updater; untouched keys are preserved by reference. reset() restores the captured initial value (lazy init is cached once), or reset(next) swaps in a provided object. patch/reset are referentially stable (safe as effect deps), and it's SSR- & StrictMode-safe. Shallow merge only — nested objects are replaced, not deep-merged; for array state use useList. Perfect for form state, settings panels, and filter/query objects.
import { useStack } from "@usefy/use-stack";
const [stack, { push, pop, peek, clear, reset }] = useStack<Snapshot>([]);
push(snapshot); // push onto the top (variadic: push(a, b, c))
const last = pop(); // pop the top and return it (undefined if empty)
peek(); // read the top without mutatingThe LIFO sibling of useQueue — identical shape, but push and pop operate on the same end (the top). LIFO semantics, immutable updates (new array on every change), a readonly T[] return type, pop that returns the popped item, stable action identities, lazy init, and no-op skipping. Read top/bottom/size directly from the stack (stack[stack.length - 1], stack[0], stack.length). Perfect for undo history, navigation/back stacks, expression evaluation, and depth-first traversals.
import { useSelection } from "@usefy/use-selection";
const {
selected, // selected items (derived from the current `items`)
isSelected, // isSelected(item) → boolean
toggle, // toggle(item)
selectAll, // select every item (no-op in single-select mode)
clear, // deselect all
isAllSelected, // every item selected (false for an empty list)
isPartiallySelected, // some-but-not-all → drive a header checkbox's indeterminate
} = useSelection(users, { getKey: (u) => u.id });Set-backed selection for checkbox lists and data tables. The backing Set stores keys (via getKey, default identity for primitives), so a selection survives new object identities across renders — rebuild items every render and the selection sticks. Item-facing values (selected, isAllSelected, …) are derived from the current items, so removing a row makes it drop out automatically. Ships toggle/select/deselect/selectAll/clear, the isAllSelected/isPartiallySelected/isNoneSelected flags for an indeterminate header checkbox, and a single-selection mode (multiple: false) that replaces the selection. Immutable Set updates, stable action identities, no-op skipping, and SSR/StrictMode safe.
⏱️ 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.
import { useTimeout } from "@usefy/use-timeout";
const { reset, clear, isPending } = useTimeout(() => {
console.log("fired");
}, 3000);
// Pass null to disable; reset() restarts, clear() cancels.Automatic cleanup on unmount, latest-callback ref, and conditional execution via a null delay.
import { useInterval } from "@usefy/use-interval";
const { start, stop, toggle, isRunning } = useInterval(() => {
tick();
}, 1000);
// Pass null to disable; { immediate: true } fires once immediately;
// { autoStart: false } starts stopped.StrictMode-safe (no leaked/duplicate timers), latest-callback ref, and automatic cleanup on unmount.
💾 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.
const [value, setValue, remove] = useCookie("theme", {
initialValue: "light",
maxAge: 60 * 60 * 24 * 7, // one week
sameSite: "strict",
});Reads/writes document.cookie with a useState-like tuple. SSR-aware, JSON (de)serialization with a raw-string fallback, and full cookie attributes (expires/maxAge/path/domain/secure/sameSite). Same-document instances sync; cross-tab writes need polling (no storage event for cookies).
📡 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.
⌨️ Keyboard
import { useKeyPress } from "@usefy/use-key-press";
// Single key — true while held
const escapePressed = useKeyPress("Escape");
// Combination (mod = Ctrl on Win/Linux, Cmd on macOS)
useKeyPress("mod+k", {
preventDefault: true,
onPress: () => openCommandPalette(),
});
// Alternative bindings — array is OR
useKeyPress(["ctrl+s", "meta+s"], {
preventDefault: true,
onPress: () => save(),
});
// Physical keys for game controls (layout-independent)
const forward = useKeyPress("w", { matchBy: "code" });
// Custom predicate
const digitPressed = useKeyPress((e) => /^[0-9]$/.test(e.key));
// Ignore shortcuts while typing in inputs
useKeyPress("f", { ignoreInputElements: true, onPress: openFilter });Perfect for command palettes, editor shortcuts, modal dismissal, and game controls — with cross-platform mod, onPress/onRelease callbacks, auto-repeat handling, and blur-safe held state.
import { useHotkeys } from "@usefy/use-hotkeys";
// Command palette (mod = Ctrl on Win/Linux, Cmd on macOS)
useHotkeys("mod+k", () => openCommandPalette(), { preventDefault: true });
// Multiple bindings for one handler (array is OR)
useHotkeys(["mod+s", "ctrl+enter"], save, { preventDefault: true });
// A Gmail-style sequence: press "g" then "i"
useHotkeys("g i", () => navigate("/inbox"));
// Scoped to an element, allowed inside inputs, only while enabled
const ref = useRef<HTMLDivElement>(null);
useHotkeys("Escape", close, { target: ref, enableOnFormTags: true, enabled: isOpen });The high-level shortcut layer over the keyboard: combos ("ctrl+shift+p", "shift+?"), space-separated sequences ("g i", "g g") with a configurable reset timeout, arrays of bindings on one handler, a cross-platform mod alias (overridable for tests), exact modifier matching ("a" never fires on Ctrl+A), and an input-field guard that ignores editable targets by default. Bind to document/window/an element/a ref. The handler is stored in a ref (no memoization needed), and it's SSR-safe and StrictMode-safe with full timer/listener cleanup.
📍 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 l