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!