Debouncing in Javascript

Debouncing is a programming practice used to improve the performance of the application by reducing the rate at which a function is called. The most common feature where debouncing is used is in search bars where an API call is made on every keystroke.

Let's take an example of a simple search bar component. I am using a react component here.

In the code block below the change handler function is called on every keystroke. So if you are searching for the name 'John Doe', your change handler will be called 8 times. If you are making an API call and fetching the search result from an API, this operation will be very expensive. To optimize this we will use debouncing.

import React, { useState } from "react";

export function FilterList({ names }) {
  const [query, setQuery] = useState("");

  let filteredNames = names;

  if (query !== "") {
    filteredNames = names.filter((name) => {
      return name.toLowerCase().includes(query.toLowerCase());
    });
  }

  const changeHandler = (event) => {
    setQuery(event.target.value);
  };

  return (
    <div>
      <input
        onChange={changeHandler}
        type="text"
        placeholder="Type a query..."
      />
      {filteredNames.map((name) => (
        <div key={name}>{name}</div>
      ))}
      <div>{filteredNames.length === 0 && query !== "" && "No matches..."}</div>
    </div>
  );
}

Why use debouncing in this scenario and how does it work?

With Debouncing we will add a delay of some milliseconds (let's consider 300ms for our example) which will prevent the api calls on every keystroke and instead will make an API call only if there is a pause of 300 milliseconds between the keystrokes. Below is a great visual to understand how debouncing works.

There are libraries out there which will provide you with debounce methods. One of the common ones is lodash. But here we will create our own debounce method using setTimeout.

 const debounce = (func, delay) => {
    let debounceTimer;
    return function () {
      const context = this;
      const args = arguments;
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(() => func.apply(context, args), delay);
    };
  };

const debouncedChangeHandler = debounce(changeHandler,300)

Let's understand the debounce function. We are taking two input arguments to the function. The first one is the function which is making the api calls on keystroke. The second one is the delay which we want to set on the calls. Please note the delay here will be the delay between the keystrokes. Our debounce function will return another function which will be executed with the set delay.

The clearTimeout is crucial for the debounce function to work as expected. As mentioned earlier, the debounced function will be triggered once there is a gap of certain defined milliseconds between the keystrokes. So we need to reset the timer on every keystroke. This is achieved through clearTimeout, we pass the timerId returned from setTimeout function to clearTimeout which clears that timer instance on the next click.

Now, Add this debouncedChangeHandler to input instead of changeHandler.

 return (
    <div>
      <input
        onChange={debouncedChangeHandler}
        type="text"
        placeholder="Type a query..."
      />
      {filteredNames.map((name) => (
        <div key={name}>{name}</div>
      ))}
      <div>{filteredNames.length === 0 && query !== "" && "No matches..."}</div>
    </div>
  );

That's all for this blog. For reference, I have added the full code below.

import React, { useState } from "react";
import faker from "faker";
let counter = 0;

export const fakeNames = Array.from(Array(5000), () => {
  return faker.name.findName();
});

export function FilterList({ names }) {
  const [query, setQuery] = useState("");

  let filteredNames = names;

  if (query !== "") {
    console.log("changehandler called ", counter++);
    filteredNames = names.filter((name) => {
      return name.toLowerCase().includes(query.toLowerCase());
    });
  }

  const changeHandler = (event) => {
    setQuery(event.target.value);
  };

  const debounce = (func, delay) => {
    let debounceTimer;
    return function () {
      const context = this;
      const args = arguments;
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(() => func.apply(context, args), delay);
    };
  };
  const deboouncedChangeHandler = debounce(changeHandler, 300);

  return (
    <div>
      <input
        onChange={deboouncedChangeHandler}
        type="text"
        placeholder="Type a query..."
      />
      {filteredNames.map((name) => (
        <div key={name}>{name}</div>
      ))}
      <div>{filteredNames.length === 0 && query !== "" && "No matches..."}</div>
    </div>
  );
}