Some events in ReactJS can run multiple times per second. Like mouse movements and window scrolling. If you need to run a function after an event like this, it's common to debounce the function, to stop it from running too often.
With debouncing, when you call a function, you give it a delay. For example, if you debounce the function with a delay of 500ms, each time you call the function it will wait 500ms before the function runs. If the same function is called again within 500ms, the function is triggered again, and the previous function call is cancelled.
This means that your function will only run after 500ms when it hasn't been called again.
If the event was a mouse move, the function will only run once per mouse move (500ms after the mouse stops moving).
Debouncing is super handy, especially if you need to run expensive functions and don't want the user's browser to hang or become unresponsive.
The custom debounce
function
Before we create the React hook, let's start with a JavaScript debounce function. One you can use with any framework or plain JavaScript.
Here's the debounce function I use - but you can also find debounce functions in JavaScript libraries like lodash
if you prefer.
// debounce function (defaults wait to .2 seconds)
const debounce = (func, wait = 200) => {
let timeout; // for the setTimeout function and so it can be cleared
return function executedFunction(...args) { // the function returned from debounce
const later = () => { // this is the delayed function
clearTimeout(timeout); // clears the timeout when the function is called
func(...args); // calls the function
};
clearTimeout(timeout); // this clears the timeout each time the function is run again preventing later from running until we stop calling the function
timeout = setTimeout(later, wait); // this sets the time out to run after the wait period
};
};
The debounce function is called debounce
(original, I know!), this is a totally custom function and you can call it whatever you like. You pass it a function func
and a delay wait
(optional, defaults to 200ms).
The debounce function works by setting a timeout, and giving that timeout a function later
to run, well, later. The timeout is set to run after the wait
time.
But (and this is what makes it a debounce function and not the native setTimeout
function), each time the debounced function is called we clearTimeout
and then setTimeout
again.
Let's see it in action:
// debounce function - runs after 1 second (1000ms)
let debounced = debounce(() => console.log("I'm debounced"), 1000);
// dummy events running every 100ms
setTimeout(debounced, 100); // this won't run
setTimeout(debounced, 200); // this won't run
setTimeout(debounced, 300); // this won't run
setTimeout(debounced, 400); // this will run
Using debounce in React
So I've seen some other useDebounce
hooks floating around the interwebs that used useEffect
to debounce state updates. The reason I ended up having to create something different is that I had some use cases where I had to debounce before setting the state.
For example, I need to run a function that did some expensive computations before updating the state. So I need to be able to call the debounce function directly from the event.
For the debounce to work, I need to call the same function each time, so the previous call gets cancelled when a new function call is triggered.
For example, this won't work:
const debounceChanges = debounce(function(newChanges) {
setChanges(complexFunction(newChanges));
}, 500); // every .5 seconds max
// example use
editor.onChanges(() => {
debounceChanges(editor.content);
});
Every time the component re-renders, a new debounceChanges function gets created. So nothing gets denounced. That's not what we want!
Luckily, we can use the useCallback
hook.
Perfect, with useCallback
, our function will now stay the same between renders.
const debounceChanges = React.useCallback(debounce(function(newChanges) {
setChanges(complexFunction(newChanges));
}, 500), []); // every .5 seconds max
// example use
editor.onChanges(() => {
debounceChanges(editor.content);
});
How to cancel the debounce
Now we have our debounce working in ReactJS, and we can be as pleased as punch. But what if you need to cancel a debounce?
I needed to do this when I was triggering an API call 10 seconds after a user last edited some content.
It looked like this:
const debounceSave = React.useCallback(debounce(function() {
handleSave();
}, 10 * 1000), []); // every 10 seconds max
React.useEffect(() => {
debounceSave();
}, [changes]); // run when new changes
React.useEffect(() => {
return () => {
handleSave(); // save on exit
}
}, [id]);
If the component unmounts or id
changes, I call handleSave()
with no debounce. But if that unmount happens after 2 seconds of an update to changes
, I need to cancel the future planned handleSave()
happening in 8 seconds (due to the debounce call that already happened 2 seconds ago).
Let's tweak the debounce
function so that we can cancel it.
// debounce function (defaults wait to .2 seconds)
const debounce = (func, wait = 200) => {
let timeout; // for the setTimeout function and so it can be cleared
function executedFunction(...args) { // the function returned from debounce
const later = () => { // this is the delayed function
clearTimeout(timeout); // clears the timeout when the function is called
func(...args); // calls the function
};
clearTimeout(timeout); // this clears the timeout each time the function is run again preventing later from running until we stop calling the function
timeout = setTimeout(later, wait); // this sets the time out to run after the wait period
};
executedFunction.cancel = function() { // so can be cancelled
clearTimeout(timeout); // clears the timeout
};
return executedFunction;
};
Now we also return a cancel
function that we can call to cancel any delayed function call we have planned. Like a Time Machine!
Here's how we can use it:
const debounceSave = React.useCallback(debounce(function() {
handleSave();
}, 10 * 1000), []); // every 10 seconds max
React.useEffect(() => {
debounceSave();
}, [changes]); // run when new changes
React.useEffect(() => {
return () => {
handleSave(); // save on exit
debounceSave.cancel(); // cancel any pending saves
}
}, [id]);
The useDebounce
custom hook
Ok, so now debounce is working in React, and our work is done! Or is it?
You may have already noticed I'm using the debounce function in two places - debounceChanges
and debounceSave
. And that's just in this one component.
I'm sure I'll find more places I need it, so let's create a custom hook!
useDebounce.hook.js
import React from 'react';
// debounce function (defaults wait to .2 seconds)
const debounce = (func, wait = 200) => {
let timeout; // for the setTimeout function and so it can be cleared
function executedFunction(...args) { // the function returned from debounce
const later = () => { // this is the delayed function
clearTimeout(timeout); // clears the timeout when the function is called
func(...args); // calls the function
};
clearTimeout(timeout); // this clears the timeout each time the function is run again preventing later from running until we stop calling the function
timeout = setTimeout(later, wait); // this sets the time out to run after the wait period
};
executedFunction.cancel = function() { // so can be cancelled
clearTimeout(timeout); // clears the timeout
};
return executedFunction;
};
// hook for using the debounce function
function useDebounce(callback, delay = 1000, deps = []) {
// debounce the callback
const debouncedCallback = React.useCallback(debounce(callback, delay), [delay, ...deps]); // with the delay
// clean up on unmount or dependency change
React.useEffect(() => {
return () => {
debouncedCallback.cancel(); // cancel any pending calls
}
}, [delay, ...deps]);
// return the debounce function so we can use it
return debouncedCallback;
};
export default useDebounce;
Perfect, now we can useDebounce
whenever we want to use debounce
! (see what I did there?!)
Here's how debounceChanges
and debounceSave
look using the new custom hook.
const debounceChanges = useDebounce(function(newChanges) {
setChanges(complexFunction(newChanges));
}, 500, []); // every .5 seconds max
const debounceSave = useDebounce(function() {
handleSave();
}, 10 * 1000, [id]); // every 10 seconds max
And the good thing about the custom hook is I no longer need to worry about cancelling any pending function calls in my component.
The hook takes care of it for me! It cancels delayed functions on dismount or if any dependencies change. In the example above debounceChanges
will cancel on dismount, and debounceSave
will cancel on dismount or id
change.