Debounce in React Functional component.

Debounce in React Functional component.

When it comes to optimizing api calls debouncing is used almost everywhere. Implementing debounce in a class-based React component is easy but it’s a little tricky in functional components. We are going to see how to implement debounce in React functional component.

Before getting to the main topic let’s understand the concept of debouncing (skip ahead if you already know what is debouncing).

What is Debounce?

Debouncing is used to add a time gap between two consecutive invocations of a function call. It forces a function to wait a certain amount of time before running again. For example, a debounce interval of 500ms means that if 500ms hasn’t passed from the previous invocation attempt, we cancel the previous invocation and schedule the next invocation of the function after 500ms.

Implementing debounce in our functional component.

Let's take an example

Our goal is to do this. our goal.gif

Here we are taking input from the user and saving it to the database when the user stops typing. For this example, we will just display the result which is going to be saved to the database.

Instead of using a loadash debounce function, we will write our own debounce function.

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

Let's create a component with an input box for the user to type something in and save it to the database only when the user stops writing.

We are also going to display two things what the user is typing right now and what is getting saved to the database.

export default function App() {
  const [input, setInput] = useState("");
  const [savetoDB, setSaveToDb] = useState("");

  const handleChange = (e) => {
   const handleChange = (e) => {
    setInput(e.target.value);
    debounce(setSaveToDb(e.target.value),500);
  };
  };

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

  return (
    <div className="App">
      <input
        type="text"
        value={input}
        onChange={handleChange}
        placeholder="type somthing..."
        style={{ marginTop: "100px" }}
      />

      <p>Typed</p>
      <p>{input}</p>
      <p>Saved To DB</p>
      <p>{savetoDB}</p>
    </div>
  );
}

This is what we get,

code executed.gif

Whatever the user is typing is getting saved immediately instead of waiting for the user to stop typing. This may lead to a lot of write requests to the database.

So, it's not working as we expected.

Why?

Every time we type a character in the input field, it fires the change event, which triggers the handleChange function, which sets the state (setState), which triggers a re-render, which recreates the handleChange function!

In React functional components, local variables inside functions are reinitialized on every render. Every time the function component is evaluated, we are registering a fresh setTimeout callback. So the debounce function is never the same in each render and so it actually cannot perform debouncing.

So we have to find a way to somehow cache our debounce function.

Here's the solution

We have to make sure that the same function is returned whenever the component is re-rendered. This can be done using React.useCallback() hook.

useCallback is commonly used for performance optimizations when passing callbacks to child components. But we can use its constraint of memoizing a callback function to ensure the delaySaveToDb references the same debounced function across renders.

 const delaySaveToDb = useCallback(debounce((val)=>{
    setSaveToDb(val)
  }
, 1000), []);


  const handleChange = (e) => {
    setInput(e.target.value);
    delaySaveToDb(e.target.value);
  };

The useCallback hook 'memoizes' a function. This means that the argument to useCallback (a function itself) will be the same instance between renders, which solves our problem.

We use a new function named delaySaveToDb in which we have used useCallback() to memoize the original debounce function. So now even if everything gets reinitialized after each render useCallback gives us the memoized denounced function which has the initial time interval.

In this way, we can implement debounce function in React functional component.

Link to the complete code is https://codesandbox.io/s/debounce-in-react-2o35n?file=/src/App.js

Happy Coding!!