How to debounce and throttle functions with React hooks
Tags: react, webdev, javascript, beginners
Published: 2021-07-27 07:18:04



**Debouncing** and **throttling** are two very common terms that we come across when trying to optimize function calls. They can be very useful for improving the performance of user interactions.

Before jumping into the main implementation, let’s understand the basic concepts of `debounce` and `throttle` and their real-life use cases (skip ahead if you are already familiar with these concepts).

-----

### What Is debounce?

Debouncing enforces that there is a minimum time gap between two consecutive invocations of a function call.

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.

A common application of `debounce` is a `Typeahead`.


### What Is throttle?

Throttling is a technique with which a function is invoked at most once in a given time frame regardless of how many times a user tries to invoke it.

For example, given a `throttle` interval of 500ms, if we try to invoke a function *n* times within 500ms, the function is called only once when 500ms has elapsed from the beginning.

`Throttle` is commonly used with `resize` or `scroll` events.

-----

## Using debounce and throttle With Class Components

Before we dive into how we can use `throttle`/`debounce` in functional components with Hooks, let’s quickly see how we do it in a class component.

We define the debounced/throttled version of our function in the constructor function, and that is all we need to do.

> Note: I am using lodash `debounce` and `throttle` functions in this article.

```javascript
import React from "react";
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.debouncedOnChange = _.debounce(this.handleChange, 300);
    this.debouncedHandleWindowResize = _.throttle(this.handleWindowResize, 200);
  }
  handleChange = (_, property) => {
    // your logic here
  };

  handleWindowResize = (_, property) => {
    // your resize logic here
  };

  // rest of rendering code
}

```

Check out the sample demo in the StackBlitz below:

{% stackblitz react-joukpk %}

----

## Using debounce and throttle in Functional Components

We shall attempt to try to convert the class-based implementation to a function-based approach.

The first thing that comes to mind is to directly define the debounced and throttled function within the functional component. So let’s try that first:

```javascript
import React from "react";
import _ from "lodash";
export default function App() {

  const onChange = () => {
    // code logic here
  };
  const handleWindowResize = () => {
    // code logic here
  };
  const debouncedOnChange = _.debounce(onChange, 300);
  const throttledHandleWindowResize = _.throttle(handleWindowResize, 300);

   //rendering code here
}

```

When we do it this way, on every render cycle of the component, a new instance of the debounced/throttled function is created. Basically, we aren’t calling the same function after each re-render and it doesn’t work as expected, which you can see in the StackBlitz demo below:

{% stackblitz react-uzpj1r %}

So this is definitely not the right way of using `debounce`/`throttle` in functional components.

------
## **Refining Our Implementation Based on Our Learning**

Now that we understand that we do not want multiple instances of our debounced or throttled function to get created after each render cycle, we shall try to optimize it. One way we can do that is by using the `useCallback` Hook.

According to the [React docs on `useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback):
> “Pass an inline callback and an array of dependencies. `useCallback` will return a memoized version of the callback that only changes if one of the dependencies has changed.”

```javascript
import React, { useState, useEffect, useCallback } from "react";
import _ from "lodash";

export default function App() {
  const [inputValue, setInputValue] = useState("");

  const onChange = () => {
    console.log('inputValue', inputValue);
    // other logic here
  };
  //debounced onChange functin
  const debouncedOnChange = useCallback(_.debounce(onChange, 300), [inputValue]);
  
  const handleWindowResize = useCallback((_, property) => {
   // logic here
  }, []);

  const throttledHandleWindowResize = useCallback(
    _.throttle(handleWindowResize, 300),
    []
  );

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

  useEffect(() => {
    onChange();
    debouncedOnChange();
  }, [inputValue]);
  
  // other code here
}

```

In the snippet above, we see that the `onChange` handler makes use of the enclosing state `inputValue`. So when we create the memoized debounced function with `useCallback`, we pass `inputValue` in the dependency array of `useCallback`. Otherwise, the values obtained in the function call will be stale values instead of the updated ones due to closures.

We have a problem, though: A new reference of our function only gets created when `inputValue` changes. However, the input value changes every time we want to call the function, so we will still face the same problem of a new reference getting created. The net result is that our function still doesn’t work as expected.

The throttled function, for its part, doesn’t use any state or enclosing variable and hence works perfectly well with an empty dependency array.

The StackBlitz below shows the same behavior:

{% stackblitz react-egaspf %}

-----
## Further Optimizing the Approach

We now know that `useCallback` can help if we are able to create the instance of the debounced or throttled function only on the initial render, so can we solve the problem of stale closures without having to add a dependency to `useCallback`?

Well, you are in luck. The answer is yes.

There are at least two ways we can solve this problem.

* Keeping a copy of our state in `ref`: Since `refs` are mutated, they aren’t truly affected by closures in the sense that we can still see the updated value even if the reference is old. So whenever we are updating the state, we also update the `ref`. We shall not go down this path unless it’s a last resort, as it is a bit hacky and involves a lot of state duplication, which isn’t ideal.

* Pass values as arguments: Instead of relying on closures to use a value, we can pass all the necessary values that our function needs as arguments.

Our code looks like this:

```javascript
import React, { useState, useEffect, useCallback } from "react";
import _ from "lodash";
export default function App() {
  const [inputValue, setInputValue] = useState("");
  const [debounceValues, setDebounceValues] = useState({
    nonDebouncedFuncCalls: 0,
    debouncedFuncCalls: 0
  });
  const [throttleValues, setThrottleValues] = useState({
    nonThrottledFunctionCalls: 0,
    throttledFuntionCalls: 0
  });

  const onChange = (property, inputValue) => {
    console.log(`inputValue in ${property}`, inputValue);
    setDebounceValues(prev => ({
      ...prev,
      [property]: prev[property] + 1
    }));
  };
  const handleWindowResize = useCallback((_, property) => {
    setThrottleValues(prev => ({
      ...prev,
      [property]: prev[property] + 1
    }));
  }, []);
  
  const debouncedOnChange = useCallback(_.debounce(onChange, 300), []);
  const throttledHandleWindowResize = useCallback(
    _.throttle(handleWindowResize, 300),
    []
  );

  const handleChange = e => {
    const value = e.target.value;
    setInputValue(value);
    onChange("nonDebouncedFuncCalls", value);
    debouncedOnChange("debouncedFuncCalls", value);
  };

  const onWindowResize = useCallback(e => {
    handleWindowResize(e, "nonThrottledFunctionCalls");
    throttledHandleWindowResize(e, "throttledFuntionCalls");
  }, []);

  useEffect(() => {
    window.addEventListener("resize", onWindowResize);
    return () => {
      window.removeEventListener("resize", onWindowResize);
    };
  }, [onWindowResize]);

  //rest of the rendering code
}

```

In the code above, we are passing the `inputValue` as an argument to the debounced function and thus ensuring that it has all the latest values it needs and works smoothly.

> **Note**: Using functional state updates can also help to avoid passing every property — especially if it just has to update the state.

Check out the full working code in the StackBlitz below:

{% stackblitz react-waor3n %}

So there we have it. Our debounced and throttled functions now work well with functional components too, and it wasn’t as complicated as we imagined it to be.

*Note: Instead of the `useCallback`, we can also use `useMemo`, but the main approach logic will remain the same.*

------------
## Summary

We performed a step-by-step conversion of a class component to a functional component using `debounce` with React Hooks. These are the key takeaways:

* We need to use the same instance of the created function as much as possible.

* Use the `useCallback/useMemo` Hook to memoize our created functions.

* To avoid closure issues and also prevent the function from getting recreated, we can pass the values needed by the function as arguments.

* State updates that need previous values can be implemented using the functional form of `setState`.


**Thank you for Reading**

Please do share your suggestions in the comments below. If you liked this article share this with your friends.

Consider following me on [Twitter](https://twitter.com/ShubhamReacts) for more tips and trips related to web development.