14May
How And When To Debounce And Throttle In React
How And When To Debounce And Throttle In React

We frequently think about speed and updates occurring as rapidly as possible when discussing how to optimize performance in a React project.  However, when updates to the UI are done too frequently, your app can be prone to performance issues. This is especially problematic in cases where user input, such as search queries or form submissions, triggers updates on every keystroke.

In React applications, certain user interactions can lead to performance issues, such as slow loading times and unresponsive UIs. For example, making asynchronous requests triggered by user input on every keystroke can overload the web server and cause it to crash, or slow down the webpage if the user is typing too fast. Similarly, calling a function with every window resize event can cause the webpage to become unresponsive.

Fortunately, there are two techniques, known as debouncing and throttling, that can help mitigate this problem. In this article, you will learn what debouncing and throttling are, how to implement them in your React applications, and why they are important for improving React performance.

Note that for this tutorial, I’ll use the Lodash library to implement debounce and throttle.

WHAT IS DEBOUNCING AND THROTTLING?

Debouncing and throttling are two techniques used to improve the performance of web applications, including those built with React. Both techniques involve controlling the frequency at which certain functions are executed.

Debouncing is the practice of delaying the execution of a function until a certain amount of time has passed since the last time it was called. This technique is often used in cases where a function is triggered repeatedly in a short amount of time, such as in response to a user typing in an input field or scrolling down a page. By debouncing the function, unnecessary or redundant calls can be avoided, resulting in better performance.

Practically, you may implement a search functionality that sends a request to the backend and return the search result as a user types in an input field initially in your React app like this:

export default function Input () {

  
const handleChange = (e) => {
    // triggered as input value changes and sends data from input field to the backend here on every keystroke
  }

  return <input onChange={handleChange} />
}

While this approach may technically work, it can significantly degrade the responsiveness of your application, particularly if the user is a fast typist. By sending a request to the backend on every keystroke, your application can quickly become overwhelmed with requests, causing it to become unresponsive. This is where debouncing comes in.

Debouncing delays the execution of a function until a certain amount of time has passed since the last time it was called. When the function is debounced, it detects every attempt to call it on every keystroke but prevents the call until a certain timer has elapsed. If the timer has not elapsed, the debounced function drops the previous call and restarts the timer, ensuring that the function is only executed when the user has finished typing.

This technique can significantly reduce the number of requests sent to the backend and improve the overall performance of the application:

import debounce from "lodash/debounce"

export default function Input = () => {

  const handleChange = (e) => {
 // triggered as input value changes and sends data from input field to the backend here on every keystroke   
  }

  const debouncedOnChange = debounce(handleChange, 500);

  return <input onChange={debouncedOnChange} />
}

Now, when a user types, the request is sent to the backend after a 500ms delay from each input. For instance, if the user types “Input”, the request is sent 500ms after “I” is typed, then another 500ms after “N”, and so on.

If the timer has not completed before the next letter is typed, the timer resets. As a result, the request is only sent to the backend after the user has finished typing their query.

Having gained a basic understanding of debouncing, let’s take a look at Throttling.

Throttling has the same concept as debouncing, in that it also delays a function call with a timer. The major difference is that the function is assured to be called on every wait interval as opposed to debouncing that constantly resets the timer on every input until a user stops typing.

Throttling is especially useful for tasks such as saving changes as you type in a blog post. If a user is a fast typist, debouncing may not be effective since the waiting time will only elapse after you finish your post. Before that happens, anything could go wrong, such as your browser refreshing and losing unsaved data.

See a practical example of debouncing and throttling here.

You can also see for yourself below:

Debounce and throttle fundamentals
Debounce and throttle fundamentals

Now that we have covered the fundamentals of both concepts, let’s take a look at how they can be effectively implemented with real-world examples in a React application. With regards to the needs of your app, you will also learn different approaches for how to appropriately debounce in a React app in the next section. When you are ready, let’s dive in.

Note that: we will only use the debounce function moving forward, considering that both functions have a nearly identical implementation.

DEBOUNCING IN REACT

The example in the previous section may have worked great and appeared to be standard React code without any errors, but they are sadly unrelated to real-world situations. In the real world, you would want to save your value in a state, maybe to pass it as a prop to another component, or send it to the backend.  Ultimately, the goal is to make use of the value in some way.

Let’s revise our previous example and include a state variable:

import debounce from "lodash/debounce";
import { useState } from "react";

export default function Input() {

  const [ value, setValue ] = useState('');

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

  const debouncedChange = debounce(handleChange, 500);

  return (  
      <input value={value} onChange={debouncedChange}/>
  );
}

The implementation in the code above utilizes the useState hook to create a state value, which is then passed to the input field. However, while the code works well in providing the initial value to the input field, it has a problem with updating the state value.

The issue lies in the debouncedChange function that is passed to the onChange handler in the input field. Due to the debounce delay, the setValue function within the handleChange function is not invoked immediately, resulting in a delay in updating the state value. This problem is demonstrated in the following example on Codesandbox.

You can also see how the value state is not updated below:

Debouncing whole onChange
Debouncing whole onChange

In order to update the state in the input field, I have to call its update function immediately, that is setValue.  This means that I can’t debounce the whole onChange callback function, in this case, handleChange.

In the event that you are sending a request to the backend, one potential approach is to debounce only the function you wish to delay rather than the update function:

import debounce from "lodash/debounce"
import { useState }  from "react"

export default function Input (){

  const [value, setValue] = useState('');

  const sendQuery = (val) => {
    // send query to the backend
  };

  // debounce the sendQuery function
  const debouncedSendQuery = debounce(sendQuery, 500);

  // state is now updated as user types because 
  // handleChange is not debounced anymore, we'll also call the debounced function here
  const handleChange = (e) => {
    const value = e.target.value;

    setValue(value);

    // call debounced function here
    debouncedSendQuery(value);
  }

  return <input value={value} onChange={handleChange} />
}

In the approach above, I have debounced just the function that sends the request to the backend, as a result, the state value can now be updated. Just that, well, that only solves half of the problem as the debouncing function is not working as intended. It still sends multiple requests to the backend, just with a delay of 500ms.

As the user types, requests will still be sent to the backend after every keystroke, just that there will be a 500ms interval between the requests. To have a better understanding of this issue, see the code demonstration here. Below, you can see how we merely just have a delay of 500ms:

Debouncing inside
Debouncing inside

Why is there still this dysfunction though?

This is due to the fact that, as you may already know, every time the value state is updated, the input component is re-rendered, and when a component is re-rendered in React, every function inside of it is called which the debounce function is no exception.

Keep in mind that a new timer is established whenever a debounce function is run. Hence, a new timer is likewise created for the debounced function and the previous timer is merely wasted in memory until its time expires and it fires its callback. This is because I am running the debounce function again on every render.

For a debounce function to efficiently work, it has to be called just once; this was not the case in this instance. Due to this, I only have a delay instead of an effective debounce function.

This dysfunction can be rectified in two ways:
1. By defining the debounce function outside the component.

2. With memoization methods: useCallback and useMemo.

Let’s examine each of these approaches in turn.

1. By defining the debounce function outside the component

The debounce method and its callback can be extracted outside of the component function since a function must be inside a component to be called when the component is rendered again. For example, this:

import debounce from "lodash/debounce"
import { useState }  from "react"

const sendQuery = (val) => {
   // send query to the backend
 };

 // debounce the sendQuery function
 const debouncedSendQuery = debounce(sendQuery, 500);

export default function Input (){

  const [value, setValue] = useState('');

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

    setValue(value);

    // call debounced function here
    debouncedSendQuery(value);
  }

  return <input value={value} onChange={handleChange} />
}

In this approach, the debounce function call is limited to just once and the code works as expected. However, since both functions are defined outside the scope of the Input component, they don’t have access to any variables defined inside the component.

In cases where the functions require dependencies that exist inside the component’s lifecycle, they have to be defined inside the component, which means we must figure out how to address the issue I encountered when I defined the functions inside. But, by using the proper memoization techniques, I can define them safely inside the component and eliminate the re-rendering issue.

We’ll examine this method in more detail in the following section.

2. With Memoization methods: useCallback and useMemo

By utilizing the memoization methods provided by React Hooks, namely useCallback and useMemo, we can improve the performance of a React application by memoizing values. These hooks take a function as an argument and return a memoized version of the function.

By utilizing these hooks, I can safely define a debounce function within a component, as demonstrated in the code example below. This approach ensures that the debounce function is only called when necessary and avoids unnecessary re-renders of the component:

import { useState, useCallback, useMemo } from 'react'
import debounce from "lodash/debounce"

export default function Input (){
  const [value, setValue] = useState("");
  
  // only gets called once after first render
  const sendQuery = useCallback((value) => {
    console.log(value);
  }, []);

  // only gets called when sendQuery changes 
  const debouncedSendQuery = useMemo(() => {
    return debounce(sendQuery, 500);
  }, [sendQuery]);

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

  return <input onChange={handleChange} value={value} />;
}

With useMemo, I have successfully memoized the debounced function. It is now called only when sendQuery or value changes and works efficiently as it should. You can see code implementations of both approaches here on Codesandbox. You can also see how it works for yourself below:

Debounce in and out min
Debounce in and out min

Conclusion

This article explained the benefits of debouncing and throttling and how they can improve web applications’ usability and performance. You also discovered when debouncing is advantageous.

In conclusion, debouncing and throttling are important techniques for improving performance in web applications. Debouncing allows us to limit the number of times a function is called, which can be useful for events that fire rapidly, such as user input. Throttling, on the other hand, limits the frequency of function calls to a set interval, which can be useful for events that need to be spaced out, such as scrolling or mouse movement.

By using these techniques, you can reduce the workload on the browser and improve the overall user experience. You have also seen how to implement debouncing and throttling in React, as well as how to use the Lodash library to simplify the process.

When used appropriately, debouncing and throttling can help to optimize web applications and make them more responsive while avoiding common issues like excessive API requests, slow loading times, and unresponsive UIs.

Credits and resources

Leave a Reply