Understanding React Concurrent Mode: Part 2

Photo by Filiberto Santillán on Unsplash

My last article discussed all the hype about the concurrent mode. It introduced what concurrent mode is and talked about all the goodness it brings. This article will demonstrate the problems with the current way of updating state and will explore how concurrent mode can help overcome all those. So without further ado, let’s dive in.

A quick recap

In the last article, we discussed that concurrent mode brings along following possibilities:

  1. Enhanced performance
  2. New UI Patterns

Now let’s see how exactly that happens. To start off, let us explore the problems first.

Note: Things may seem to get complicated. I personally had a bit of a hard time figuring out stuff. A piece of advice is to just understand what is happening. You should be able to see everything happening. Don’t worry about how things are happening. We will cover that in later articles.

Blocking rendering

The problem

As we know, once a state update starts re-rendering the app, it cannot be stopped. That can be problematic and can adversely affect user experience and performance. Let us discuss the example given in the React docs.

Imagine a long list of items. Say that the list is filterable. This means that you can search for items in the list. What if that list of items is extremely long? You find that the app feels laggy. To be precise, the search bar “stutters” whenever you type into it. Why does that happen? (Here is an example of the same)

To understand what goes on over there, we consider how state is updated. But before that, a bit about controlled components.

Controlled components

An HTML input element maintains its state internally (outside React — in the DOM). That state is accessible by event.target.value inside various events of that input element (event.target.value gives the updated value, after the event has occured). Since the input element maintains it’s own state outside React, you end up with taking care of states in different places : in your React app and in the DOM. React has no way to access the DOM state directly from the input as the flow of data is unidirectional — Parent to children. This means that inside your React app, you do not know about the state of that input element. There is no way to sync those states. To resolve this, React introduced a pattern called controlled components.

To being with, you use a state variable in your React app. That variable will be mapped to the input. In HTML, value property of an input refers to its initial value. But in React, value prop refers to the value that is to be rendered by the input. Initial value can be set by defaultValue prop instead. The input element renders whatever value is given to it in the value prop. So, the input element renders the value stored in React state and not in its internal state (As said, that value is passed to the element viavalue prop). You can use the input events to update the React state variable. Below example illustrates the process.

Consider a controlled text input. Let us understand what happens when we type into it:

  1. Initially, it renders the value that is given to it in value prop. That value is essentially a state variable passed to the input via value prop.
  2. When you a type character into it, its internal state is updated. It still renders the value from the value prop. But the internal state reflects the new value.
  3. You access the updated value from onChange prop by using event.target.value and update your app’s corresponding state variable with that value.
  4. As a state update causes re-render, input is rendered with the new state variable value. So, the input reflects the new value. This makes your React app’s state the single source of truth.

The input is hooked into your app. Changing the input will cause the app to re-render with that new input value. Different parts of your app can depend on that input value and re-render whenever the input changes. You can use the input declaratively in the app.

What happened in the filterable list example?

The app stores the search term (search input value) and the list of items to be displayed in the state. The following steps are taken whenever you type into the text input:

  1. The state of app is updated to reflect the new value of the text input. App is re-rendered, reflecting the new value in the text input (controlled input).
  2. When the text input value changes, list is filtered. The filtered list is then stored in the state of the app. The component re-renders, showing the filtered list.
  3. Since filtering of list is expensive, it takes considerable time (To simulate this, I run a very long empty loop before every list update). The state update corresponding to filtering the list takes time. During that time, app’s state cannot be changed.
  4. The app is blocked. You cannot make changes to the app during the list update. So if you type something into the input, it does not update immediately. The corresponding state update is queued.
  5. When the list is updated, the queued update is executed. So the input is updated. Since the update was not immediate, it felt like the app “stuttered”.
  6. Since the input value was updated, list is filtered again — blocking the app. This goes on and on as long as you type.
React.useEffect(
() => {
setList(List.filter(item => {
if (searchTerm){
let i = 0;
while (i <= 10000000) i++;
return item
.title
.toLowerCase()
.includes(searchTerm.toLowerCase());
}
else return true;
}));
}, [searchTerm]);

Take a moment. Read the above steps again till you can see everything clearly (Here’s the source code if you wish to take a peek). Now here’s a fun experiment. You know what concurrent mode is, right? How can concurrent mode help us here?

Normally, people use one of the two techniques to tackle this:

  1. Debounce: When the user types, do not immediatly update the list. Instead, wait till the user is done typing and then update the list. But this can be irritating as the list does not update as you type.
  2. Throttle: Alternatively, you can update the list. But limit the update frequency. So, list will update only a given number of times in a given time interval and not on every key press.

Both of the above methods improve the performance but result in a “sub-optimal” user experience.

The solution

Imagine if you worked with concurrent mode. There are essentially two state updates happening — updating the input field and updating the list. Under concurrent mode, the flow would be following:

  1. You type something in the input. That causes a state update (due to change in the corresponding state variable) and as a result, the app re-renders.
  2. Since the input was updated, list is filtered as per the new value. List state update is expensive. But it does not block the app. That update is being rendered “in different universe”. Since the app does not block, you can freely use the input.
  3. You type in something in the input. Keep in mind that the list is still being updated. But since that update is in a “different universe”, you can easily update your input. So the input state update and the old list update occur concurrently.
  4. The input is updated to the new value while the old list update occurs. The update to input triggers another list update. The old update might be going on all this time.
  5. The new update to the list is queued as the old update is still going on. Note that since we are updating the same variable, we make those updates in order and not concurrently. When the old update is ready, list corresponding to it is rendered. The new update now starts rendering.
  6. This goes on and on. You can update the list and the input concurrently. Because of that, you can change the input while the list state is being updated. The app does not feel laggy or stuttering.

This might be a lot to take in. I suggest that you read it again till you get it. Don’t worry if you do not understand it right now. In further articles, I will demonstrate how this happens with an example.

Better loading sequences

The problem

Most of the modern SPAs (single page applications) have multiple screens. If you were to implement this in React, you’d take following actions:

  1. Use a state variable to tell you which screen to render. Set its initial value corresponding to the initial screen.
  2. Render the content of the app conditionally. That is, check the value of that state variable and the render the corresponding screen.
  3. When you wish to go to a new screen, change the state variable’s value and set it corresponding to the new screen.
  4. This update to state causes the app to re-render. As soon as that happens, old screen disappears. Since you render the content of screen conditionally, new screen is renderd.

Now imagine if the new screen isn’t ready yet. For example, the new screen needs to fetch data from an API before it can display reasonable content. In that case, you do not have the old screen. And the new screen cannot display meaningful content. The best you can do is show a loading state (a spinner for example). But if there are too many loading screens, it degrades user experience. (An example of this can be found here)

How can concurrent mode help you there? Take a moment to think about it (This is a tricky one).

The solution

In concurrent mode, the following would happen:

  1. You want to navigate to a new screen. So you trigger a state update for the new screen. But that update takes place in a “different universe”.
  2. You do not lose the old state. So you stay at the current screen. You can show a loading indicator to show that the update is taking place in “another universe”. Remember that you don’t have to understand how things are happening. Just understand what is happening for now.
  3. When the new screen has loaded enough data to display meaningful content, you let the state update use the DOM and render the new screen.

This is a much better UX. Want to see something like this in action? Head over to the React’s documentation. Using your browser’s developer tools, disable cache and throttle network speed to simulate a slow network. Reload the page. Now when you go from one link to another, you see a loading indicator in the top right corner. The new screen loads when it is ready.

I noticed this little change around mid-2019. I shrug it off as a routing feature and thought nothing much of it. But when I read about the concurrent mode, I realised that I had already seen something like it in action! (To be honest, I am not sure if they used concurrent mode to make it work. But it is similar User Experience)

How does it all fit in?

We have seen two (abstract) examples of concurrent mode. For now, let us see how our understanding of concurrent mode fits in above discussion. In concurrent mode, React can render updates concurrently — in async. This means that multiple updates occur together. With a bit of synchronization, we can control how updates render. We will see how to do that in later articles, when we implement the concurrent mode.

Concurrent mode simply means the ability to render multiple updates together and then commit them to DOM as needed

Keeping that in mind, consider the first example. If an update is going on and another more “urgent” update arrives (interaction based updates), we can let the urgent update commit first and delay the ongoing update. We saw how crucial that can be in the filterable list example.

Let us look at the second example now. When we want to go to a new screen, we start rendering the update corresponding to it in memory. Since the update is async, current state is not lost. We can stay on the current screen and wait for the update to finish. When the next screen is ready, we can commit it to DOM, which cause new screen to be painted on the screen. Commiting of the update is effectively delayed.

Looking at these examples and the definition mentioned in the beginning of this section, everything fits in smoothly. To quote the React docs,

React uses a heuristic to decide how “urgent” an update is, and lets you adjust it with a few lines of code so that you can achieve the desired user experience for every interaction

This is all for now. In the next article, we explore the all new Suspense and the enhancements it brings. Till then, peace ; )

CSE Undergrad | NIT Bhopal | All things Web

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store