Natural debouncing using the useDeferredValue hook in React 18

Amit Merchant · July 7, 2022 ·

Oftentimes, when working with UIs, you may come across scenarios where you need to render something based on the user-entered input.

The scenario

A good example of this is to search through a list of data. Let me show you how you would do this in React using an example.

Take a look at the following React.js app.

import { useState } from "react";

const ListUsers = ({ name, id }) => {
  return <p key={id}>{name}</p>;
};

export default function App() {
  const users = [
    {
      id: 1,
      name: "Amit Merchant"
    },
    {
      id: 2,
      name: "John Doe"
    },
    {
      id: 3,
      name: "Cherika Merchant"
    },
    {
      id: 4,
      name: "James Bond"
    },
    {
      id: 5,
      name: "Jake Sully"
    }
  ];

  const [searchTerm, setSearchTerm] = useState("");

  return (
    <>
      <h1>List of users</h1>
      <input
        type="search"
        placeholder="Search users"
        onChange={(e) => setSearchTerm(e.target.value)}
      />

      <h2>Search term: {searchTerm}</h2>

      {users
        .filter(({ name }) => {
          return (
            searchTerm.length === 0 ||
            name.toLowerCase().includes(searchTerm.toLowerCase())
          );
        })
        .map((user) => (
          <ListUsers key={user.id} name={user.name} />
        ))}
    </>
  );
}

As you can tell, this simple app loops through a static array of users using the Array.map method and lists all the users using the <ListUsers> component.

We also have a search input that can be used to filter out users based on the user input. For this, we’re maintaining a searchTerm using a useState hook and updating the state of it on search input’s onChange event.

Since we are passing searchTerm to Array.filter before looping through the users array, this will re-render the current component and in turn, its child component <ListUsers> will be re-rendered as well.

Here’s everything in action.

The problem

Now, this setup would work just fine if we’re straight up filtering out the list. But consider the scenario where the <ListUsers> component is doing some heavy computation before rendering the user detail. This would make the <ListUsers> component a little sluggish to render as well as the search input from which the user input is being fed.

To do this, we can place a while loop in the <ListUsers> before the rendering happens that simulates some complex computation like so.

const ListUsers = ({ name, id }) => {
  let now = performance.now();
  while (performance.now() - now < 40) {
    // This loop is intentially here just to drag the component
    // down in a hard running loop.  It could represent something
    // like a complex calculation involving drawing a city shape
    // or something else compute intensive. It's mean to represent
    // work that can not be easily optimized or removed.
  }

  return <p key={id}>{name}</p>;
};

Now, if we try to search through the users’ list using the search input, we may find it a little slow while typing in it. That’s because of the while loop that simulates complex computations.

This is a bad UX since the user is getting blocked while the other component is trying to render something. This shouldn’t be the case.

To fix this, we can use a debounce function that can defer the user input for the specified amount of time. This can make the overall experience a little better.

Or if we’re using React 18, we can make use of the new useDeferredValue hook to debounce things… but in a more streamlined manner.

The useDeferredValue hook

Essentially, the useDeferredValue hook is React’s answer to handle debouncing natively. It accepts a value and returns a new copy of the value that will defer to more urgent updates like so.

import { useDeferredValue } from "react";

const deferredValue = useDeferredValue(value);

How this works is, under the hood, the useDeferredValue hook will return the previous state (deferred value) of the value passed into it for the current render of that component and then render the new value after the urgent render has completed. Like displaying filtered users’ lists in our case.

The difference between a usual debouncing and this hook is React will intelligently work on the update as soon as other work finishes instead of a fixed amount of time that we specify in a debouncing function.

Using the useDeferredValue hook

So, if we want to incorporate the useDeferredValue hook in our previous example to defer the user input for user search all we need to do is to pass in the searchTerm to the useDeferredValue hook like so.

const [searchTerm, setSearchTerm] = useState("");

const deferredSearch = useDeferredValue(searchTerm);

The returned value (deferredSearch) from the hook then can be used to filter the user’s list like so.

<h2>Search term: {deferredSearch}</h2>

{users
.filter(({ name }) => {
    return (
    deferredSearch.length === 0 ||
    name.toLowerCase().includes(deferredSearch.toLowerCase())
    );
})
.map((user) => (
    <ListUsers key={user.id} name={user.name} />
))}

That’s it! That’s all we need to defer the value of searchTerm using the useDeferredValue hook.

Now, if we try to search through the input, it won’t feel sluggish even if the child component <ListUsers> is doing heavy-duty computation. The search input will function normally and the deferred value of it will be used to filter out the users.

Putting it all together, here’s how the entire example that uses useDeferredValue would function like.

Try searching the user through the input and notice the “Search term:” where it shows the entered input as you type. You’ll see a minor lag between that and the user input. Especially when you use the backspace key to remove a character but not while typing the search term itself!

👋 Hi there! I'm Amit. I write articles about all things web development. If you like what I write and want me to continue doing the same, I would like you buy me some coffees. I'd highly appreciate that. Cheers!

Comments?