Creative development studio based in Mytilene, Greece.

We use cookies to improve your experience.

Implementing useDeferredValue in React: A Step-by-Step Approach

AG Software Development

4 minutes

Implementing useDeferredValue in React: A Step-by-Step Approach

React and its vibrant ecosystem provide many tools to create impressive and functional UIs - that are responsive to users' actions. This reactivity (no pun intended) may come with a performance cost, depending on how we are handling the relationship between the UI's reactivity and the user's actions.

A simple use case

Picture this - You have a list of users or maybe goods, and the user types into an input to filter through them. Of course, performance may not be an issue when you have 50 or 150 items, but what happens when the list's length scales past 5000?

The premise is that, as the user types, the list is filtered to only show the items that match the query criteria. However, constantly responding to user input and performing expensive computation comes at a cost. Typically, we could debounce or throttle code execution, but this could potentially introduce additional complexity.

This is where useDeferredValue comes in, to keep the code clean and concise.

Wait, what is the difference between debounce, throttle and useDeferredValue?

Simply put, both debouncing and throttling are techniques that delay the execution of a function. What sets them apart is how that is done.

Throttling effectively limits how often a function is invoked, by setting a specific time interval. This typically happens by keeping track of the last invocation and calling it again conditionally, after the specified interval has passed. In this case, we could limit the list filtering to only occur every 150ms for instance. It is quite typical to see either custom implementations or, more often, to see useThrottle in action.

Debouncing, on the other hand, allows us to delay a function's invocation until the user stops performing an action for the amount of time we specify. This can be achieved by either implementing your own debounce, or using an existing solution, such as Lodash's debounce.

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

export const MyComponent = () => {
    const [userQuery, setUserQuery] = useState<string>("");

    /* rest of your code here... */

    const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
        setUserQuery(e.target.value);
    }

    const handleDebouncedChange = useCallback(debounce(handleChange, 300), []);

    <input 
        onChange={handleDebouncedChange} 
        type="text" 
        placeholder="Search"
      />
};

useDeferredValue approaches this differently. It essentially defers updating a specific part of the UI, while being integrated with Suspense. Syntax-wise, it is fairly trivial - it is called on the top level of the component, and accepts either a primitive value, or objects that have been created outside the component's rendering.

import { useState, useDeferredValue } from 'react';

export const MyComponent = () => {
  const [userQuery, setUserQuery] = useState<string>("");
  const deferredUserQuery = useDeferredValue(userQuery);

   /* rest of your code here... */
}

How does it work under the hood?

To illustrate this, we can think of ParentComponent, which holds the input and userQuery, and a large <CustomList /> component, which re-renders. On the initial render, once the ParentComponent mounts, deferredUserQuery will have the same value as userQuery.

Assuming the user then starts typing, this means that userQuery has now changed and useDeferredValue schedules a re-render, which is interruptible. During this moment, if the user is interacting with the input faster than the <CustomList/> can re-render, the <CustomList/> will only re-render after the user stops typing.

Disclaimer: The data shown below have been generated with Faker and are mock data.

a white paper with a bunch of different items on it

Stale data and styling

Providing constant feedback to the user is essential for good UX.

Since useDeferredQuery is integrated with Suspense, we can use the latter to show the loading state between re-renders.

Additionally, we can also indicate whether the presented data are stale or not, by simply comparing the userQuery and deferredUserQuery values.

import { useState, useDeferredValue } from 'react';

export const MyComponent = () => {
  const [userQuery, setUserQuery] = useState<string>("");
  const deferredUserQuery = useDeferredValue(userQuery);

   /* rest of your code here... */
  const isRenderedDataStale = deferredUserQuery !== userQuery;

  return (
    <article className={cn("text-neutral-950 font-medium", isRenderedDataStale ? "opacity-50" : "opacity-100")}>
     ....
    </article>
  )

The verdict

As with most things, this is not a one-size-fits-all solution. useDeferredValue provides a really clean and intuitive way of handling UI responsiveness by preventing unnecessary re-renders and by providing direct feedback to the users. However, you should always keep in mind that using it depends on the occasion - if you are looking to reduce HTTP requests, for instance, debouncing / throttling is the way to go, as it does not handle such cases.