DEV Community

Cover image for How to make a custom Debounce hook in react js
Rajesh Royal
Rajesh Royal

Posted on

How to make a custom Debounce hook in react js

I could have use the Loadash naaahh... 🙅‍♀️ Loadash is too expensive.

So here is how you can make your own debounce effect with custom hooks in react js.

updated as per @lukeshiru comment

useDebouncedEffect.tsx

import { DependencyList, EffectCallback, useEffect } from "react";

export const useDebouncedEffect = (
   effect: EffectCallback, 
   delay: number, 
   deps?: DependencyList
) => {
  useEffect(() => {
    const handler = setTimeout(() => effect(), delay);

    return () => clearTimeout(handler);
    // using || operator because 
    // if its optional then it can be undefined.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [...(deps || []), delay]);
};

Enter fullscreen mode Exit fullscreen mode

To use this hook simply call It like this:

import { useDebouncedEffect } from "./useDebouncedEffect";

  // debounce search onchange
  // eslint-disable-next-line react-hooks/exhaustive-deps
  useDebouncedEffect(
    () => {
       // function which will be running on effect, 
       // in our case when something changes in search box.
       changeSearchState();
    },
    // time to wait before effect runs
    debounceTimeInMilliseconds
    // this is the dependency, if it changes it will trigger 
    // the debounce again
    [search]
  );
Enter fullscreen mode Exit fullscreen mode

credit - from Internet [R&D]

Thanks for reading this far 😃

Fact: Do you know you can hit that follow button on top right and make it disappear 😄. Do not believe me try once 👍

Top comments (5)

Collapse
 
lukeshiru profile image
Luke Shiru

Using TypeScript, you should avoid any as much as possible, so a fixed version would look more like this:

import { useEffect } from "react";
import type { EffectCallback, DependencyList } from "react";

export const useDebouncedEffect = (
    effect: EffectCallback,
    deps: DependencyList,
    delay: number,
) =>
    useEffect(() => {
        const handler = setTimeout(effect, delay);

        return () => clearTimeout(handler);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [...deps, delay]);
Enter fullscreen mode Exit fullscreen mode

About the actual implementation, this wouldn’t produce a great DX mainly because we need to pass a dependency list even if we want to runt it "always", so deps ideally should be optional as it is for useEffect, but then we have the problem that you put delay as the last argument, and that one is required. Ideally the argument order should change like this:

import { useEffect } from "react";
import type { EffectCallback, DependencyList } from "react";

export const useDebouncedEffect = (
    effect: EffectCallback,
    delay: number,
    deps?: DependencyList,
) =>
    useEffect(() => {
        const handler = setTimeout(effect, delay);

        return () => clearTimeout(handler);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [...deps, delay]);
Enter fullscreen mode Exit fullscreen mode

And you could even make it into a curried function and keep it pretty similar to a regular hook:

import { useEffect } from "react";
import type { EffectCallback, DependencyList } from "react";

export const useDebounce =
    (delay: number) => (effect: EffectCallback, deps?: DependencyList) =>
        useEffect(() => {
            const handler = setTimeout(effect, delay);

            return () => clearTimeout(handler);
            // eslint-disable-next-line react-hooks/exhaustive-deps
        }, [...deps, delay]);
Enter fullscreen mode Exit fullscreen mode

And then you use it like this:

import { useDebounce } from "./useDebounce";

// Outside your component:
const use1SecondDebounce = useDebounce(1_000);

// Inside your component:
use1SecondDebounce(changeSearchState, [search]);
Enter fullscreen mode Exit fullscreen mode

Still, my recommendation would be to use a library for this, like the pretty good use-debounce.

Cheers!

Collapse
 
rajeshroyal profile image
Rajesh Royal • Edited

And you could even make it into a curried function and keep it pretty similar to a regular hook:

Not useful in this case, yes we can suppress TS warning but It will not be a good idea.

React Hook useEffect cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.

React Hook useDebouncedEffect cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.

Collapse
 
lukeshiru profile image
Luke Shiru • Edited

The linter doesn't know how are you using your code so it gives you those warnings, if you use it as I said (first call of the curried version outside the component, second inside) it works exactly the same as a regular hook. Linters are there to help, but they aren't silver bullets, you have to understand the reason for the warnings and errors you get.

Collapse
 
rajeshroyal profile image
Rajesh Royal • Edited

Didn't tried the use-debounce but the loadsh's Debounce was having some state persistence issue.

Your comment is super helpful, thanks man 🤘

Collapse
 
pratikdungarani profile image
pratikdungarani • Edited

export function useDebouncedValue(value, wait) {
const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
     const id = setTimeout(() => setDebouncedValue(value), wait);
     return () => clearTimeout(id);
  }, [value]);
Enter fullscreen mode Exit fullscreen mode

return debouncedValue;

}