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

use-debouncy

v6.0.0

Published

πŸŒ€ Small (~0.2kb) debounce effect hook for React with TypeScript support

Readme

useDebouncy

πŸŒ€ Small (~0.2kb) debounce effect hook for React with TypeScript support

GitHub npm bundle size npm types FOSSA Status

Features

  • πŸ‘Œ No dependencies.
  • πŸ‹οΈβ€ Tiny. ~0.2kb.
  • 🦾 Frame-aligned. Driven by requestAnimationFrame.
  • πŸ“– Types. Written in TypeScript, ships its own declarations.
  • 🎣 Three hooks. Debounce an effect, a callback or a value.

Installation

npm install use-debouncy
yarn add use-debouncy
pnpm add use-debouncy

Requires React 18 or 19 as a peer dependency.

Usage

Three hooks, one idea: do the work only after things have stopped changing for a while. Pick the one that fits what you already have.

Debounce an effect

Reads like useEffect, but waits for the dependencies to settle. It does not run on the initial render.

import { useState } from 'react';
import { useDebouncyEffect } from 'use-debouncy';

const Search = () => {
  const [value, setValue] = useState('');

  useDebouncyEffect(
    () => fetchData(value), // called once typing stops
    400, // milliseconds to wait
    [value], // dependencies, like useEffect
  );

  return (
    <input value={value} onChange={(event) => setValue(event.target.value)} />
  );
};

Like useEffect, the callback may return a destructor. It runs before the next invocation and when the component unmounts:

useDebouncyEffect(
  () => {
    const controller = new AbortController();
    fetchData(value, { signal: controller.signal });

    return () => controller.abort();
  },
  400,
  [value],
);

Debounce a callback

Returns a function that delays the call until it stops being called. Handy for event handlers, where there is no dependency array to speak of.

import type { ChangeEvent } from 'react';
import { useDebouncyFn } from 'use-debouncy';

const Search = () => {
  const handleChange = useDebouncyFn(
    (event: ChangeEvent<HTMLInputElement>) => fetchData(event.target.value),
    400,
  );

  return <input onChange={handleChange} />;
};

The returned function keeps the same identity across renders, so it is safe to hand to a memoized child or to put in a dependency array. It also carries two controls for the call that is currently waiting:

const save = useDebouncyFn(saveDraft, 1000);

save.flush(); // run the pending call now
save.cancel(); // drop it instead

Debounce a value

The shortest path when the value already lives in state β€” you get a copy that trails behind until things go quiet.

import { useEffect, useState } from 'react';
import { useDebouncyValue } from 'use-debouncy';

const Search = () => {
  const [value, setValue] = useState('');
  const query = useDebouncyValue(value, 400);

  useEffect(() => {
    fetchData(query);
  }, [query]);

  return (
    <input value={value} onChange={(event) => setValue(event.target.value)} />
  );
};

Recipes

Autosave that survives navigation

Debounce the writes while typing, and flush the last one when the user leaves.

const Editor = ({ id }: { id: string }) => {
  const [draft, setDraft] = useState('');
  const save = useDebouncyFn((text: string) => saveDraft(id, text), 1000);

  // Nothing is lost on unmount: the pending write goes out immediately
  useEffect(() => () => save.flush(), [save]);

  return (
    <textarea
      value={draft}
      onChange={(event) => {
        setDraft(event.target.value);
        save(event.target.value);
      }}
    />
  );
};

Validation that stops when the field is cleared

const Email = () => {
  const [email, setEmail] = useState('');
  const [error, setError] = useState<string | null>(null);

  const validate = useDebouncyFn((value: string) => {
    setError(isEmail(value) ? null : 'Enter a valid email');
  }, 500);

  return (
    <input
      value={email}
      onChange={(event) => {
        const { value } = event.target;
        setEmail(value);
        setError(null);

        // No point validating an empty field, so drop the pending run
        if (value) validate(value);
        else validate.cancel();
      }}
    />
  );
};

API

useDebouncyEffect(fn, wait?, deps?)

function useDebouncyEffect(
  fn: EffectCallback,
  wait?: number,
  deps?: DependencyList,
): void;

| Argument | Required | Default | Description | | -------- | -------- | ------- | ------------------------------------------------------------- | | fn | βœ“ | | Called once the dependencies settle. May return a destructor. | | wait | | 0 | Milliseconds to wait. | | deps | | [] | Dependencies, as in useEffect. |

Skips the initial render, restarts the timer whenever the dependencies change, and cancels a pending call on unmount.

useDebouncyFn(fn, wait?)

function useDebouncyFn<Args extends unknown[]>(
  fn: (...args: Args) => unknown,
  wait?: number,
): DebouncyFn<Args>;

| Argument | Required | Default | Description | | -------- | -------- | ------- | ------------------------------------------------- | | fn | βœ“ | | Called with the arguments of the last invocation. | | wait | | 0 | Milliseconds to wait. |

Returns a function with a stable identity, which also exposes:

| Method | Description | | ---------- | ------------------------------------------ | | cancel() | Drops the pending call, if any. | | flush() | Runs the pending call immediately, if any. |

useDebouncyValue(value, wait?)

function useDebouncyValue<Value>(value: Value, wait?: number): Value;

| Argument | Required | Default | Description | | -------- | -------- | ------- | --------------------- | | value | βœ“ | | Value to trail. | | wait | | 0 | Milliseconds to wait. |

Returns the value unchanged on the first render, then updates once it has stopped changing.

Types

import type { DebouncyFn } from 'use-debouncy';

type DebouncyFn<Args extends unknown[]> = ((...args: Args) => void) & {
  cancel: () => void;
  flush: () => void;
};

Good to know

The timer follows frames, not the wall clock. Waiting is driven by requestAnimationFrame, so the callback lands right before a repaint, together with the rest of the frame's work. The flip side: browsers pause frames for hidden tabs, so a pending call resumes only when the tab becomes visible again. Work that has to happen while the user is away belongs in visibilitychange or sendBeacon instead.

Server rendering works. Nothing touches browser globals during render, so renderToString is fine β€” the timer only ever starts in the browser.

You may not need this package. If the only goal is to keep a heavy render from blocking typing, React's own useDeferredValue does that with no dependency at all. Reach for these hooks when you want to delay a side effect: a request, an autosave, a validation.

Testing. Fake timers have to mock requestAnimationFrame as well, or the callback never fires. jest.useFakeTimers() and Playwright's page.clock.install() both do.

Development

yarn install
yarn hooks      # once per clone: installs the pre-commit hook

yarn dev        # story gallery at http://localhost:3100/playwright/gallery/index.html
yarn test       # component tests in Chromium, Firefox and WebKit
yarn test --ui  # same, in Playwright's UI mode
yarn lint       # oxlint + oxfmt
yarn typecheck  # tsc
yarn build      # bundle into lib/

Components under test live in playwright/stories/*.story.tsx, one export per scenario, and the tests address them by id. The gallery is served by the project's own Vite dev server and renders stories in StrictMode, so React's development-only behaviour is covered too. Open the gallery URL in a browser to eyeball every story by hand.

Commit messages follow Conventional Commits β€” they decide the released version.

License

MIT Β© Egor Avakumov

FOSSA Status