React now has concurrency support with the release of version 18. There are numerous features now that help to make better use of system resources and boost app performance. One such feature is the useDefferedValue
hook, in this article we’re going to learn about useDeferredValue
and understand the scenarios where we can use it.
Why do we need useDefferedValue
?
Before we can see this hook in action, we need to understand something about how React manages state and updates the DOM.
Let’s say we have the following code:
App.js
export default function App() {
const [name, setName] = useState('');
const computedValue = useMemo(() => {
return getComputedValue(name);
}, [name]);
const handleChange = (event) => {
setName(event.target.value);
};
return (
<input
type="text"
placeholder="Username"
value={name}
onChange={handleChange}
/>
);
}
Here we create a state variable with the useState
hook, and a computed value (computedValue
) derived from the state. We use the useMemo
hook to recalculate the computed value only when the state changes.
So when the value of the input field changes, the name
state variable is updated and the computed value is recomputed before the DOM is updated.
This usually isn’t an issue, but sometimes this recalculation process involves a large amount of computation and takes a long time to finish executing. This can reduce performance and degrade the user experience.
For example, we could be developing a feature that lets a user search for an item in a gigantic list:
App.js
function App() {
const [query, setQuery] = useState('');
const list = useMemo(() => {
// 👇 Filtering through large list impacts performance
return largeList.filter((item) => item.name.includes(query));
}, [query]);
const handleChange = (event) => {
setQuery(event.target.value);
};
return (
<>
<input type="text" value={query} onChange={handleChange} placeholder="Search"/>
{list.map((item) => (
<SearchResultItem key={item.id} item={item} />
))}
</>
);
}
In this example, we have a query state variable used to filter through a huge list of items. The longer the list, the more time it will take for the filtering to finish and the list
variable to be updated for the DOM update to complete.
So when the user types something in the input field, the filtering will cause a delay in the DOM update and it’ll take time for the text in the input to reflect what the user typed immediately. This slow feedback will have a negative effect on how responsive your app feels to your users.
I simulated the slowness in the demo below so you can better understand this problem. There are only a few search results for you to properly visualize it, and they’re each just the uppercase of whatever was typed into the input field.
In this demo, I am typing each character one after the other as quickly as I can, but because of the artificial slowness, it takes about a second for my keystroke to change the input text.
useDeferredValue
in action
This is a situation where the useDeferredValue
hook is handy. useDeferredValue()
accepts a state value as an argument and returns a copy of the value that will be deferred, i.e., when the state value is updated, the copy will not update accordingly until after the DOM has been updated to reflect the state change. This ensures that urgent updates happen and are not delayed by less critical, time-consuming ones.
function App() {
const [query, setQuery] = useState('');
// 👇 useDefferedValue
const deferredQuery = useDefferedValue(query);
const list = useMemo(() => {
return largeList.filter((item) => item.name.includes(query));
}, [deferredQuery]);
const handleChange = (event) => {
setQuery(event.target.value);
};
return (
<>
<input type="text" value={query} onChange={handleChange} placeholder="Search" />
{list.map((item) => (
<SearchResultItem key={item.id} item={item} />
))}
</>
);
}
In the example above, our previous code has been modified to use the useDeferredValue
hook. As before, the query
state variable will be updated when the user types, but this time, useMemo
won’t be invoked right away to filter the large list, because now deferredQuery
is the dependency useMemo
is watching for changes, and useDeferredValue
ensures that deferredQuery
will not be updated until after query
has been updated and the component has been re-rendered.
Since useMemo
won’t be called and hold up the DOM update from the change in the query
state, the UI will be updated without delay and the input text will change once the user types. This solves the responsiveness issue.
After the query
state is updated, then deferredQuery
will be updated, causing useMemo
to filter through the large list and recompute a value for the list
variable, updating the list of items shown below the input field.
As you can see in the demo, the text changes immediately as I type, but the list lags behind and updates sometime later.
If we keep changing the input field’s text in a short period (e.g., by typing fast), the deferredQuery
state will remain unchanged and the list will not be updated. This is because the query
state will keep changing before useDeferredValue
can be updated, so useDeferredValue
will continue to delay the update until it has time to set deferredQuery
to the latest value of query
and update the list.
Here’s what I mean:
This is quite similar to debouncing, as the list is not updated till a while after input has stopped.
Tip
Sometimes in our apps, we’ll want to perform an expensive action when an event occurs. If this event happens multiple times in a short period, the action will be done as many times, decreasing performance. To solve this, we can set a requirement that the action only be carried out “X” amount of time since the most recent occurrence of the event. This is called debouncing.
For example, in a sign-up form, instead of sending a request once the user types to check for a duplicate username in the database, we can make the request get sent only 500 ms since the user last typed in the username input field (or of course, we could perform this duplicate check after the user submits the form instead of near real-time).
Since the useDeferredValue
hook defers updates and causes additional re-render, it’s important that you don’t overuse it as this could actually cause the performance problems we’re trying to avoid, as it forces React to do extra re-renders in your app. Use it only when you have critical updates that should happen as soon as possible without being slowed down by updates of lower priority.
Conclusion
The useDeferred
value accepts a state variable and returns a copy of it that will not be updated until the component has been re-rendered from an update of the original state. This improves the performance and responsiveness of the app, as time-consuming updates are put off to a later time to make way for the critical ones that should be shown in the DOM without delay for the user to see.
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.