What is a debounce function and how do I use it in React?
Written by our consultant Sovanny Huy
The other day I was tasked with doing a code test for a front end assignment. The test was pretty straight forward — build a React application with a search feature for finding characters, using an API (fictional characters involved in intergalactic warfare, you might have heard about it). The application would, among other things, be judged on performance — specifically the avoidance of unnecessary API calls.
I told a friend of mine about the test and they immediately preached “use a debouncer!”. I knew it had something to with optimizing search bars, and after some research, I realized what a brilliant piece of algorithm it is.
What does it do?
In a nutshell, a debounce function makes sure a function isn’t called until the number of calls has “calmed down”. A few examples:
Don’t run an onclick handler until the user has stopped mashing a button
Waiting for a mousemove to stop
Wait until the user has “paused” typing into search bar
The latter is my scenario. I don’t want an API call to be made for every letter typed into the searchbar.
How does it work?
Let’s take a look at an ES6 version of the algorithm and break it down line by line.
// A function that returns a function // and takes in a callback function and a // delay in milliseconds as arguments function debounceEvent(callback, delay) { // This will be a timer ID returned by setTimeout let timer; // The closure created by the nested functions // retains access to each timer return (...args) => { // Everytime it is called, cancel previous timer // (If previous timer fired, nothing will happen here) clearTimeout(timer); // Create a new timer with delay milliseconds until // callback is called timer = setTimeout(() => { callback(...args); }, delay); }; }
A neat function (at least without the comments). Now let’s implement it in my React search bar component.
How to implement it in React
I added my debounce function in my file with utility functions, and exported it from there. I then imported it into my search bar component into a function variable with my API call and 300ms as arguments. The function is called for every onChange in the search input. This should make the API call only fire when the user stopped typing for 300ms. However, it did not work as expected.
const debouncedSearch = debounceEvent(val => { apiSearchReq(val); }, 300); function handleOnChange(event) { //... debouncedSearch(event.target.value); }
Logging every debounce function call and every actual API call, I found that my search bar still fired an API call for every keystroke, after 300ms each. It turns out, a new instance of debouncedSearch is created for each re-render. It hit me that that’s how React works and the reason we need hooks.
Hooks to the rescue
This is the perfect opportunity to use the useCallback
hook, because referential equality matters. Definition of useCallback
from React Docs:
Pass an inline callback and an array of dependencies.
useCallback
will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.
In this case, we want to prevent unnecessary API calls, and we only want one version of our callback so we pass in an empty dependency array. Our final debouncedSearch
looks just slightly different from before:
const debouncedSearch = useCallback( debounceEvent(val => { apiSearchReq(val); }, 300), [] );
Thanks to the useCallback
hook, my search bar works as expected. To demonstrate, I’ve console logged the user input as well as the creating, clearing and firing of timers along with their ids. (The first one is undefined due to the order of the code expressions. This is fine.)
So there you have it, a use case of debouncing, and how to implement it with React (using hooks).
Written by our consultant Sovanny Huy.