npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@usefy/hooks

v0.2.5

Published

A collection of useful React hooks

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/hooks

Individual 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-storage

Peer 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 | | 100% | | @usefy/use-counter | Counter state with increment, decrement, reset | | 100% | | @usefy/use-debounce | Value debouncing with leading/trailing edge | | 92% | | @usefy/use-debounce-callback | Debounced callbacks with cancel/flush/pending | | 93% | | @usefy/use-throttle | Value throttling for rate-limiting updates | | 100% | | @usefy/use-throttle-callback | Throttled callbacks with cancel/flush/pending | | 100% | | @usefy/use-local-storage | localStorage persistence with cross-tab sync | | 95% | | @usefy/use-session-storage | sessionStorage persistence for tab lifetime | | 95% | | @usefy/use-click-any-where | Document-wide click event detection | | 92% | | @usefy/use-copy-to-clipboard | Clipboard copy with fallback support | | 88% | | @usefy/use-event-listener | DOM event listener with auto cleanup | | 96% | | @usefy/use-on-click-outside | Outside click detection for modals/dropdowns | | 98% | | @usefy/use-unmount | Execute callback on component unmount | | 100% | | @usefy/use-init | One-time initialization with async, retry, timeout | | 96% | | @usefy/use-timer | Countdown timer with drift compensation and formats | | 84% | | @usefy/use-geolocation | Device geolocation with real-time tracking and distance | | 90% | | @usefy/use-intersection-observer | Element visibility detection with Intersection Observer | | 94% | | @usefy/use-signal | Event-driven communication between components | | 98% | | @usefy/use-memory-monitor | Real-time browser memory monitoring with leak detection | | 90% |


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 pending
const 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; // boolean

Perfect 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: useSignal is 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 at coverage/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