Debouncing, Throttling, and Concurrent Features in React 18

How and when to use each of these performance optimisation techniques in modern React applications.

Introduction

Debouncing and throttling are two different techniques used in web development to control the frequency of a function's execution in response to user input or other events. There are a lot of great articles on the web that explain these techniques, and this blog post assumes that you're already familiar with them (if not, check out this post). I will also not cover how to implement debounce and throttle functions with vanilla JS (this post explains it very well). I'd like to point out that implementing them with vanilla JS is one of the most common questions for front-end interviews. It's definitely worth knowing how to do that, but I wouldn't recommend writing them in production yourself. Just use something like Lodash's debounce and throttle functions. They just work, and their APIs allow you to customise their behaviour to suit different use cases.

I will cover these topics:

  1. How to debounce/throttle callbacks in React using a library called use-debounce. This is a very popular library, and I think just using its hooks instead of built-in React hooks will be a good choice for most use cases.

  2. How to debounce/throttle callbacks using built-in React hooks (with or without Lodash's debounce and throttle functions). Other tech bloggers have already covered this subject well in my opinion, so I'll just point you to some good posts to have a look at.

  3. When useTransition and useDeferredValue hooks, which were added in React 18, should be used instead of debouncing or throttling. Both hooks rely on React's new concurrent renderer that was introduced in v18. This blog post from the React team explains it in great depth if you're interested.

Let's get into it.

Debouncing/throttling

Option 1: use-debounce

This is the quickest and easiest way to debounce/throttle your functions. Have a look at the examples of how to use it on the project's GitHub page. The library is very popular (over a million downloads per week – npm) and has 2.6k stars on GitHub. It's actively maintained and works fine with the latest versions of React. Additionally, its size is very small (< 1 Kb), and it's compatible with Lodash's implementations of throttle and debounce functions. This matters because Lodash allows you to control whether your throttled or debounced function will execute on the leading and/or trailing edge of your debounce/throttle period (aka the wait timeout). It is important to be able to control this for some use cases. Please have a look at Lodash docs for debounce() and throttle() to learn more about the subtle details of their behaviour depending on the parameters that you call them with.

To summarise, I don't see why not just go with use-debounce for most use cases. If you need to customise how debouncing/throttling works for your use case in a way that the library doesn't allow you to (unlikely), then you should consider other options.

Option 2: DIY Using Built-In React Hooks

I think this subject has already been covered quite well by other authors, so if you're curious how to do this, have a look at these blog posts:

  1. https://dmitripavlutin.com/react-throttle-debounce/ – how to use Lodash's throttle and debounce functions with useCallback or useMemo to debounce/throttle callbacks. It's worth noting that use-debounce uses useMemo under the hood, as you can see in the source code. They provide some comments on why they chose to use this React hook in the comments in the source code file.

  2. https://dmitripavlutin.com/controlled-inputs-using-react-hooks/#4-debouncing-the-controlled-input – how to debounce a state change using useEffect without Lodash's functions (using setTimeout and clearTimeout).

useTransition and useDefferedValue Hooks

These two hooks were added in React 18. The official docs provide lots of info and examples of when and how to use each of them (useTransitions docs, useDeferredValue docs). As I already mentioned, both of them rely on React's new concurrent renderer. Both hooks can be used to optimise performance in certain cases where the only option was to use debouncing or throttling before React 18. I think this video explains React's evolution and both of these hooks very well, so I highly recommend watching it if you're new to them.

Both of these hooks achieve the same goal using the same method. The goal is performance optimisation of the work that happens during rendering. The method is prioritising which updates are more important and interrupting lower-priority work using the new concurrent renderer.

Let me explain when you should use them over debouncing/throttling. The idea is very simple – if the work you’re optimising happens during rendering, you should use these new hooks. Debouncing and throttling are still useful if that's not the case (e.g. they can let you send fewer network requests as a user clicks a button multiple times or types into a search input). For example, let's say you have a long list of items in memory. You also have a search input, and you want to filter the list as a user types. There's a risk that the application will start lagging when they start typing something into the input. Debouncing is a good solution – just wait for them to stop typing for something like a second and only then update the list. But the better way to address this is to use one of these new hooks. The advantages of using them over debouncing/throttling are:

  1. The new hooks don't require a fixed delay.

  2. The lower-priority updates (e.g. rendering the updated list) are interruptible and won't block the higher-priority/urgent updates (e.g. updating the search input).

How quickly the list will be updated will depend on a user's device instead of a fixed delay. If they're using a good PC, the deferred update will happen almost immediately. If their device is a budget Android smartphone, the list would “lag behind” the input proportionally to how slow the device is. So, you can tell React which updates should be prioritised higher, and which updates should be treated as non-urgent using one of these hooks. For a more comprehensive discussion, please read this explanation in the official React docs.

When to Use Which Hook?

Let's look at some code examples to understand when to use each of these hooks. They shouldn't be used together, because they achieve the same goal.

1. useDefferedValue should be used when you don't have access to the state-updating code. It wraps a value that is affected by a state change. For example, let's say we have this component:

function ItemList(props) {
  const items = useDeferredValue(props.items);
  return (
    <ul>
      {items.map((item) => (
        <li>{item}</li>
      ))}
    </ul>
  );
}

In this case, there's a parent component that has the search input and it passes the filtered list of items to the ItemList child component. You can think of useDefferedValue as a way to tell React to defer updating a value until it finishes all higher-priority updates. Other updates are all considered urgent by default. The result is that the application will remain responsive – a user will be able to continue typing without any lagging (because React will allow the urgent text input update to interrupt the non-urgent list update), and how quickly the list is updated will depend on their hardware.

2. useTransition should be used when you have access to the state-changing code. It lets you explicitly tell React which updates are a lower priority. A good example can be found in the docs. The state-changing code wrapped inside of a startTransition will be treated by React as a lower priority.

As you can see, it's pretty straightforward to use both of these hooks.

Conclusion

To recap, debouncing and throttling are still very useful techniques, and you can apply both of them either using built-in React hooks or a library like use-debounce. Additionally, with the arrival of React 18, there's now a better way of optimising the work that happens during rendering using the new hooks to tell React which updates should be treated as a lower priority. Since they are deeply integrated with React and take a user's hardware into account, developers can now optimise the performance of their applications in a way that was not possible before v18. It's also worth noting that the new concurrent renderer will only apply to those components in an application in which concurrent features (like the hooks discussed) are used. The other components in an application will still re-render as in React v17 and prior versions.