TypeOfNaN

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"
  }
]

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli