Asynchronous rendering with useDeferredValue

How React 18 can change our mental model of React rendering

Nicolas Li
OVRSEA
5 min readApr 21, 2022

--

Photo by Lautaro Andreani on Unsplash

I recently discovered my mental model of React was wrong while working on a complex app. Here is a simplified version of it:

The common belief is that anytime a component’s prop changes, it re-renders immediately. So I’d expect here that as soon as counter changes, <ExpensiveRenderComponent/>and <BasicComponent/>re-render and display their respective content. Given that <ExpensiveRenderComponent/>has some costly processing to do before displaying its content, I’d expect <BasicComponent/> to render first. But this is not the actual behaviour: both components are updated exactly at the same time. In other words, <BasicComponent/>waits for <ExpensiveRenderComponent/>to show up.

Both component get updated at the same time even though <BasicComponent/> is ready before <ExpensiveRenderComponent/>

I spent quite some time on this problem and finally figured out what was missing in my mental model of React. Here we need to dive deeper into how rendering works.

React rendering is synchronous

Whenever a component’s state changes, React triggers a re-render of the latter and all of its children. Three steps happen during this process:

  1. React first computes the new virtual DOM induced by the state change. The virtual DOM is basically the Javascript representation of the user interface and is equivalent to the actual DOM. The only difference here is that the virtual DOM is not displayed so it’s faster to manipulate.
  2. When the new virtual DOM has been computed, React compares it to the actual DOM and updates the parts which have actually changed. This process is called reconciliation.
  3. As a final step, React applies the changes to the actual DOM. It’s called the commit phase.
React rendering in 3 steps

Computing the new virtual DOM implies running the code of every component which are rendering because of the state change. Hence this step’s duration is at least as long as the time needed to compute the most complex component.

In our example, <ExpensiveRenderComponent/>and <BasicComponent/> re-render because of the same state change. <ExpensiveRenderComponent/> needs to perform some costly calculation and therefore React has to wait for it to be complete before updating the actual DOM. During this time, the UI becomes unresponsive.

Rendering is therefore synchronous. This behaviour was also designed for the sake of consistency. React will always update components re-rendering because of the same state change at the same time. The doc explains it well in the documentation:

This makes sense in the vast majority of situations. Inconsistent UI is confusing and can mislead users. (For example, it would be terrible if a messenger’s Send button and the conversation picker pane “disagreed” about which thread is currently selected.)

One solution is to optimize the component which requires the most time to render. But what if it is impossible? This is where React 18 comes in!

Photo by Igor Savelev on Unsplash

React 18 Concurrent Features

React 18 was released in March 2022 and adds in powerful features for concurrent rendering. Before, rendering was synchronous as our previous example showed: each rendering component triggered by a state change would have to wait for all the others to be ready to be updated. With React 18, different priorities can be given to each state change, or even to each component rendering as we’ll see. In a nutshell, React 18 enables asynchronous rendering.

Let’s see how we can apply the new hook useDeferredValue to our former problem:

Here, useDeferredValue creates a deferred version of counter, which allows to decouple the rendering of<ExpensiveRenderComponent/> and <BasicComponent/>:

  • <BasicComponent/> can render immediately with the new counter .
  • Meanwhile, deferredCounter keeps the old value of counter until React has finished the calculation of <ExpensiveRenderComponent/> with the new value of counter , without blocking the main thread. When it’s done, <ExpensiveRenderComponent/> gets updated immediately and the app is completely up-to-date.

Let’s see this hook in action:

<BasicComponent/> gets updated immediately while <ExpensiveRenderComponent/> takes its time to render

Great, the UX has greatly improved since the application is much more responsive! However, you can notice that the UI consistency has been sacrificed since <ExpensiveRenderComponent/> displays the stale value of counter for a short time. The solution to this problem is to notify the user when the UI is not up-to-date. It can be as simple as introducing a prop isStale like this:

Now it’s trivial to make the UI consistent by returning a loading state when isStale is true:

The UI is consistent again thanks to the prop isStale

Takeaways

  • Having a consistent mental model of React is key not only to understand weird behaviours of your app but also to enhance it efficiently.
  • React rendering is synchronous and enforces UI consistency. In return, the UI can become unresponsive if some components take time to render.
  • React 18 concurrent features enable asynchronous rendering. useDeferredValue can defer the render of costly components and make the UI more responsive that way. The trade-off is that UI consistency might be sacrificed if it’s not handled correctly.
  • There are many other features for concurrent rendering in React 18: useTransition , Suspense… Check them out!

Resources

--

--