DEV Community

loading...
Cover image for Debounce Any Effect by Creating Your Own useDebouncedEffect Hook

Debounce Any Effect by Creating Your Own useDebouncedEffect Hook

Nick Scialli (he/him)
Husband, dog dad, software engineer, coffee monster. Working in civic tech!
・2 min read

Often, we want to wait until our user finishes an action to execute an asynchronous effect. A great example of this is executing a search after a user finishes typing rather than doing so on each key stroke. This prevents us from jarring UI changes or firing many unnecessary and potentially expensive fetch requests.

In this post, we'll write a custom React hook that debounces any effect!

Writing Our Hook

Our hook should look just like a useEffect hook, with the exception that it should take an additional time parameter for the amount of time we want to debounce for. Therefore, the parameters should be:

  • The effect function
  • The dependency array
  • The debounce time

Achieving the debounce behavior

To debounce, we'll use a setTimeout with the user-provided time. The catch is that, if our effect re-runs before the timeout executes, we'll want to cancel the timeout and start a new one. We can accomplish that by using a cleanup function with clearTimeout. Our hook, therefore, is as follows:

import { useEffect } from "react";

function useDebouncedEffect(fn, deps, time) {
  const dependencies = [...deps, fn, time] 
  useEffect(() => {
    const timeout = setTimeout(fn, time);
    return () => {
      clearTimeout(timeout);
    }
  }, dependencies);
}

Seeing the Hook in Action

In this example, we'll simply set some state on a debounced delay based on when a user stops typing in a textbox. Here's the code!

function App() {
  const [text, setText] = useState("")
  const [debounced, setDebounced] = useState("")

  useDebouncedEffect(() => {
    setDebounced(text);
  }, [text], 1000)

  return (
    <div className="App">
      <input onChange={e => {
          setText(e.target.value)
        }} value={text} 
      />
      <p>{debounced}</p>
    </div>
  );
}

And when we try it in action... it works!

Text being debounced

Discussion (3)

Collapse
gyandeeps profile image
Gyandeep Singh

Nice example. It was fun playing around with it.

It works in this simplified case but if their is someother higher priority update going on in the same component it will not work. The issue is that the instance of fn is always new which triggers the useEffect inside useDebouncedEffect. I think using useCallback for that would fix the issue.

function useDebouncedEffect(fn, deps, time) {
  const dependencies = [...deps, fn, time];
  useEffect(() => {
    const timeout = setTimeout(fn, time);
    return () => {
      clearTimeout(timeout);
    };
  }, dependencies);
}

export default function App() {
  const [text, setText] = useState("");
  const [debounced, setDebounced] = useState("");
  const [count, setCount] = useState(0);

  const deb = useCallback(() => {
    setDebounced(text);
  }, [text]);

  useDebouncedEffect(deb, [text], 1000);

  // to mock a higher priority frequent update
  useEffect(() => {
    setInterval(() => {
      setCount((c) => c + 1);
    }, 500);
  }, []);

  return (
    <div className="App">
      <input
        onChange={(e) => {
          setText(e.target.value);
        }}
        value={text}
      />
      <p>{debounced}</p>
      <p>{count}</p>
    </div>
  );
}
Collapse
nas5w profile image
Nick Scialli (he/him) Author

I hadn’t thought about this! I’m going to try it out an amend the post later :). Thank you!

Collapse
gyandeeps profile image
Gyandeep Singh

Not sure in every case people gonna run into this but their is a chance.