Debouncing with React Hooks
Nick Scialli
April 09, 2019
In this post, I am going to use the React Hooks API to implement debouncing. If you are unfamiliar with debouncing, it is simply a mechanism to postpose the execution of a function. Typically, this will be implemented in javascript to prevent a potentially costly operation (e.g., requesting data from the backend) from being executed prematurely. An example of debouncing in real life is a typeahead list that only executes a search some amount of time after a user stops typing.
Setup
I will use an example in which we filter a JSON file of US states based on user input. We don’t want to filter the list until the user stops typing, so we will want to implement debouncing. Our JSON file will be in the following format:
[
{
"name": "Alabama",
"abbreviation": "AL"
},
{
"name": "Alaska",
"abbreviation": "AK"
},
...
]
For simplicity, we’ll just filter the name
field based on user input.
Implementing filtering hooks without debouncing
Let’s quickly implement the functionality without debouncing. This means the list of states will be filtered automatically as the user types.
Note: This is not an introduction to hooks post; if the following code seems entirely foreign to you, you may want to read the React Hooks Introuction documentation.
import React, { useState, useEffect, Fragment } from 'react';
import states from './states';
const App = () => {
const [search, setSearch] = useState('');
const [filteredStates, setFilteredStates] = useState(states);
useEffect(() => {
const filter = states.filter((state) => {
return state.name.toLowerCase().includes(search.toLowerCase());
});
setFilteredStates(filter);
}, [search]);
return (
<Fragment>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<ul>
{filteredStates &&
filteredStates.map((state) => {
return <li key={state.abbreviation}>{state.name}</li>;
})}
</ul>
</Fragment>
);
};
export default App;
When our input element is changes, setSearch
is called, which changes our search
value, re-rendering the component. The useEffect
hook runs as we have indicated it should whenever search
changes. Within the useEffect
hook, we filter states
based on the search
value.
If we run our application at this point, we see it functions as desired.
Add debouncing
To add debouncing, we can intuit that the useEffect
hook should be updated with a timer. Let’s add a setTimeout
with a 1 second wait and see how that works.
useEffect(() => {
setTimeout(() => {
const filter = states.filter((state) => {
return state.name.toLowerCase().includes(search.toLowerCase());
});
setFilteredStates(filter);
}, 1000);
}, [search]);
This isn’t quite right. It does delay the filtering, but it doesn’t wait until you stop typing. Essentially, the app now just waits a second and starts catching up to what you’re typing. This is not the ideal behavior–the exact same amount of filtering is happening.
There is a simple fix: the useEffect
hook offers a “cleanup” mechanism that will run when the component re-renders next. All we have to do is return the cleanup function from first function passed to the useEffect
hook. In our example, we simple need to cancel the setTimeout
. If the 1 second has not passed before the user types more, the setTimeout
will be canceled and the filter will not be applied.
useEffect(() => {
const timer = setTimeout(() => {
const filter = states.filter((state) => {
return state.name.toLowerCase().includes(search.toLowerCase());
});
setFilteredStates(filter);
}, 1000);
return () => clearTimeout(timer);
}, [search]);
Success! We now wait until 1 second after the last user input to filter the states. The full code can be viewed below.
App.js
import React, { useState, useEffect, Fragment } from 'react';
import states from './states';
const App = () => {
console.log('render');
const [search, setSearch] = useState('');
const [filteredStates, setFilteredStates] = useState(states);
useEffect(() => {
const timer = setTimeout(() => {
const filter = states.filter((state) => {
return state.name.toLowerCase().includes(search.toLowerCase());
});
setFilteredStates(filter);
}, 1000);
return () => clearTimeout(timer);
}, [search]);
return (
<Fragment>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<ul>
{filteredStates &&
filteredStates.map((state) => {
return <li key={state.abbreviation}>{state.name}</li>;
})}
</ul>
</Fragment>
);
};
export default App;
states.json
[
{
"name": "Alabama",
"abbreviation": "AL"
},
{
"name": "Alaska",
"abbreviation": "AK"
},
{
"name": "American Samoa",
"abbreviation": "AS"
},
{
"name": "Arizona",
"abbreviation": "AZ"
},
{
"name": "Arkansas",
"abbreviation": "AR"
},
{
"name": "California",
"abbreviation": "CA"
},
{
"name": "Colorado",
"abbreviation": "CO"
},
{
"name": "Connecticut",
"abbreviation": "CT"
},
{
"name": "Delaware",
"abbreviation": "DE"
},
{
"name": "District Of Columbia",
"abbreviation": "DC"
},
{
"name": "Federated States Of Micronesia",
"abbreviation": "FM"
},
{
"name": "Florida",
"abbreviation": "FL"
},
{
"name": "Georgia",
"abbreviation": "GA"
},
{
"name": "Guam",
"abbreviation": "GU"
},
{
"name": "Hawaii",
"abbreviation": "HI"
},
{
"name": "Idaho",
"abbreviation": "ID"
},
{
"name": "Illinois",
"abbreviation": "IL"
},
{
"name": "Indiana",
"abbreviation": "IN"
},
{
"name": "Iowa",
"abbreviation": "IA"
},
{
"name": "Kansas",
"abbreviation": "KS"
},
{
"name": "Kentucky",
"abbreviation": "KY"
},
{
"name": "Louisiana",
"abbreviation": "LA"
},
{
"name": "Maine",
"abbreviation": "ME"
},
{
"name": "Marshall Islands",
"abbreviation": "MH"
},
{
"name": "Maryland",
"abbreviation": "MD"
},
{
"name": "Massachusetts",
"abbreviation": "MA"
},
{
"name": "Michigan",
"abbreviation": "MI"
},
{
"name": "Minnesota",
"abbreviation": "MN"
},
{
"name": "Mississippi",
"abbreviation": "MS"
},
{
"name": "Missouri",
"abbreviation": "MO"
},
{
"name": "Montana",
"abbreviation": "MT"
},
{
"name": "Nebraska",
"abbreviation": "NE"
},
{
"name": "Nevada",
"abbreviation": "NV"
},
{
"name": "New Hampshire",
"abbreviation": "NH"
},
{
"name": "New Jersey",
"abbreviation": "NJ"
},
{
"name": "New Mexico",
"abbreviation": "NM"
},
{
"name": "New York",
"abbreviation": "NY"
},
{
"name": "North Carolina",
"abbreviation": "NC"
},
{
"name": "North Dakota",
"abbreviation": "ND"
},
{
"name": "Northern Mariana Islands",
"abbreviation": "MP"
},
{
"name": "Ohio",
"abbreviation": "OH"
},
{
"name": "Oklahoma",
"abbreviation": "OK"
},
{
"name": "Oregon",
"abbreviation": "OR"
},
{
"name": "Palau",
"abbreviation": "PW"
},
{
"name": "Pennsylvania",
"abbreviation": "PA"
},
{
"name": "Puerto Rico",
"abbreviation": "PR"
},
{
"name": "Rhode Island",
"abbreviation": "RI"
},
{
"name": "South Carolina",
"abbreviation": "SC"
},
{
"name": "South Dakota",
"abbreviation": "SD"
},
{
"name": "Tennessee",
"abbreviation": "TN"
},
{
"name": "Texas",
"abbreviation": "TX"
},
{
"name": "Utah",
"abbreviation": "UT"
},
{
"name": "Vermont",
"abbreviation": "VT"
},
{
"name": "Virgin Islands",
"abbreviation": "VI"
},
{
"name": "Virginia",
"abbreviation": "VA"
},
{
"name": "Washington",
"abbreviation": "WA"
},
{
"name": "West Virginia",
"abbreviation": "WV"
},
{
"name": "Wisconsin",
"abbreviation": "WI"
},
{
"name": "Wyoming",
"abbreviation": "WY"
}
]
Nick Scialli is a senior UI engineer at Microsoft.