What Is Debouncing?

What Is Debouncing?
Photo by Samuel-Elias Nadler / Unsplash

Let me tell you a story.

Years ago, I was holding an exam for my students. The exam was a task in JavaScript where they needed to implement a search input that would display songs returned from Apple's iTunes API endpoint. I prepared everything for them and they started coding.

Suddenly, one student asked for help as he was having strange errors in the code. Then all the other students started asking the same. I saw that it was a 403 error or something similar.

What happened is that the first student had a bug in the code that executed API calls to iTunes very frequently and they got his IP address blocked. Since all computers in the classroom shared an IP address, all the other students experienced the same.

So they almost sabotaged the exam, but I just changed the API endpoint for their exam task and let them continue with coding.

If this first student correctly implemented debouncing, this wouldn't have happened.

Debouncing

Debouncing is a technique used by engineers to remove unnecessary noise in hardware or software. Software engineers use it to ensure the execution of a specific code doesn't happen too often.

That specific code is usually heavy and expensive. Sometimes it can cause performance issues or bugs if not done right. Examples are API calls, database queries, heavy asynchronous operations, etc.

As a software engineer, you always want to be careful and provide optimal app performance. Because of that, it's important to know when and how to implement debouncing.

There are a lot of scenarios when debouncing is needed:

  • API rate limiting - eg. a search input functionality where you need to call some API endpoint to fetch search results
  • button clicks - a user (or bot) can click the button multiple times in very short times
  • forms - you can execute validation many times for each entered character
  • window resizing event - when a window is resized it can trigger many events
  • scrolling - when users scroll the timeline component and you call functions for analytics - you want to ensure

How to implement debouncing?

Let's say we need to implement the same exam task.

This is how we can do it in React:

import { useEffect, useState } from "react";
import axios from "axios";

import SongItem from './components/SongItem";
import SearchError from './components/SearchError";

const DELAY_MS = 300;

export default function SearchInput() {
  const [searchTerm, setSearchTerm] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const [error, setError] = useState(false);
  
  useEffect(() => {
    const getMusicResponse = setTimeout(() => {
      axios
        .get(`https://api.searchmusic.com/songs/${searchTerm}`)
        .then((response) => setSearchResults(response.data))
        .catch((error) => setError(error));
    }, DELAY_MS);

    return () => clearTimeout(getMusicResponse);
  }, [searchTerm]);

  if(error){
    return <SearchError error={error}/> 
  }
  
  return (
    <div className="search-term-wrapper">
      <input
        className="search-input"
        placeholder="Search songs..."
        onChange={(event) => setSearchTerm(event.target.value)}
      />
      {searchResults.map((song) => <SongItem key={song.id} song={song}>}
    </div>
  );
}

So instead of calling the API every time the setSearchTerm function is called, for each character entered by a user, the API is now called 300 milliseconds after a user enters the last character.

A useEffect hook will run every time searchTerm value changes, so when the user types something in the search input.

In this useEffect hook, there is a function called getMusicResponse. This function gets a result from setTimeout function. The callback function inside the setTimeout will fetch the data from the API after a certain delay. The delay is set for 2 seconds.

The instance of the useEffect hook must be nullified by using return, followed by clearTimeout, every time it finishes.

Let's say the user keeps writing, so each key release triggers the getMusicResponse again. Every call will reset the timer, or, in other words, cancel the previous execution of the callback function inside setTimeout that fetches the data and reschedule it for a new time, 300 ms in the future.

This goes as long as the user keeps pressing the keys under 300 ms. The last execution won’t get cleared, so the callback will finally be called and data will be fetched.

If you need a debounce function, you can code your own or use lodash and underscore.