Understanding React Concurrent Mode: Part 1
The engineers at Facebook have never failed to amaze me. I have been in love with React for almost a year now. GraphQL is one of the most mesmerizing things I have ever come across. Relay is alright but gets better everytime. Their work is often very well thought and targets real-world problems intimately. This means that either the people at Facebook do really awesome work or that I get swayed away very easily (Both are not mutually exclusive of course). But this isn’t just my opinion. The rate at which React is being adopted to design user interfaces is enough evidence of all the goodness it brings along (MDN switching to React is just one of those numerous cases).
The people at Facebook have kept up with the pace — learning from personal experience and feedback. This is why I was excited to see a new section under the React documentation. Concurrent mode tries to curb a suite of issues at a base level and abstract it all away as a part of the design. This series of articles tries to explore this in more detail than the official documentation. This article introduces concurrent mode. I try to cover most of the content assuming that the reader is a beginner (Therefore, feel free to skip the parts that you already know about).
Note: The concurrent mode is still an experimental feature. You don’t need to know about it to start working with React. This series is aimed at people who are curious about it and need a resource that talks about it more deeply than the documentation.
UPDATE (13–01–2020): The code structure has been changed. It is no longer deployed on a sandbox. Necessary changes have been highlighted in the article : )
A little background
Before getting into what concurrent mode is, let us talk about a little about state in React. React documentation has always advised treating the setting of state
(setState) as an asynchronous action. If you do not understand how asynchronous actions work, read the corresponding section below (I could add an anchor to that section but where’s the fun in that? Nahh, there is no “solid” way to do that and I always try avoid workarounds). In current versions of React,
setState is performed in async to batch or defer state updates. Beginners often ignore this and run into bugs.
What happens when state is updated?
React re-renders after every state update. An app exists as a hierarchy of components (in the form of a tree). When a component is updated, following steps are taken:
- React recalculates the part of the tree (subtree) that is affected by the update. To do that, it must locate the subtree first.
- If you think carefully, this is a relatively easy task. The component being updated is the root of the subtree. All of its children form the rest of the subtree.
- Now that subtree is located, React traverses the subtree (starting from root). As it traverses a node in the old subtree, it creates the corresponding node in the new subtree.
- When it reaches a node, it checks the type of that node. If the type of node is a component, react “renders” it. In class components, appropriate lifecycle methods (including render method) are called. In function components, the function body is executed. It then moves on to all the children of this node, repeating the process recursively. The node in the new subtree is then created by the result of this render. This part of the state update process is called the render phase.
- Now we have a new component tree. We also have a new virtual DOM corresponding to this component tree. Next step is to update the actual DOM to match the virtual DOM.
- For that, React uses a heuristic algorithm and diffs (comparing files in order to determine how or whether they differ) the DOM with virtual DOM. Under some very practical assumptions, the complexity of the diffing algorithm is
Ois the big-o notation and
nis the number of elements in the tree).
- After diffing, the DOM is updated. This is the commit phase of the update. Diffing makes it possible to update the DOM efficiently, with minimal computation.
This diagram sums up the lifecycle methods and phases of update process pretty well. Note that the actual update process might differ from the above description. But the steps are more or less the same.
The commit phase takes negligible time, thanks to React’s diffing algorithm. But the render phase may contain expensive computation and is often time-consuming. Applications that frequently update their state put a high load on the browser and can often seem slow & laggy.
Batching and deferring of state updates
A state update is triggered when
setState is called. State update is considered an async process. That is, when you call
setState, you do not know when exactly the state will update. This enables React to batch and defer updates.
- Deferring: When a state update is scheduled by
setState, it may not be triggered at that very moment. Instead, React may defer the state update i.e., postpone it. That is done to facilitate batching.
- Batching: Multiple state updates can be applied together, as a single state update. The updates are grouped and applied collectively. React uses this to enhance performance.
When are state updates deferred and/or batched? React considers this to be an implementation detail. As per the documentation, it currently batches the updates that arise as a result of the same event. Let us consider an example. Note that the below example is a bit complicated and you may not get it in the first attempt. In case that happens, try reading it again calmly. Alternatively, you can skip it.
Say a click in child component causes an event handler to run in the child. That handler calls
setState in the parent component. You might be familiar with this pattern if you know about lifting the state up. Now, due to bubbling of the click event, it is also triggered for the parent. Say parent has an event handler too (for the click event). Say that it also sets the state inside the parent. In that case, we have two state updates.
If those updates were to applied one by one, the following would happen:
- The child will re-render. This is the result of the
setStatecall that child’s event handler makes.
- The parent will re-render. This is due to the
setStatecall made by the parent’s event handler.
- When the parent re-renders, the child will re-render too. So the child renders twice.
To avoid that, React delays all the state updates that result from the same event. Afterwards, it collects all the updates and applies them at once. In that case, only a single render occurs which reflects all the changes. Event here is not restricted to classic DOM events. It also includes lifecycle events i.e., any state updates that occur as a result of lifecycle event may also be batched together. Below code-snippet explores it practically.
Note: This example uses the class syntax for clarity. But other example will use hooks as they are more convenient for both people and machines. They make reusing stateful logic easier. They also allow for better division of concerns inside a component. All the snippets are written in TypeScript. If you are not familiar with TypeScript, there are a few things you can do. They are discussed later.
T̶h̶e̶ ̶b̶e̶l̶o̶w̶ ̶s̶a̶n̶d̶b̶o̶x̶ ̶c̶o̶n̶t̶a̶i̶n̶s̶ ̶a̶l̶l̶ ̶t̶h̶e̶ ̶e̶x̶a̶m̶p̶l̶e̶s̶ ̶a̶n̶d̶ ̶s̶n̶i̶p̶p̶e̶t̶s̶ ̶t̶h̶a̶t̶ ̶I̶ ̶d̶i̶s̶c̶u̶s̶s̶ ̶i̶n̶ ̶t̶h̶e̶ ̶e̶n̶t̶i̶r̶e̶ ̶s̶e̶r̶i̶e̶s̶ ̶o̶f̶ ̶a̶r̶t̶i̶c̶l̶e̶s̶.̶ ̶C̶o̶n̶s̶i̶d̶e̶r̶ ̶t̶h̶e̶ ̶A̶s̶y̶n̶c̶h̶r̶o̶n̶o̶u̶s̶ ̶S̶t̶a̶t̶e̶ ̶U̶p̶d̶a̶t̶e̶ ̶e̶x̶a̶m̶p̶l̶e̶
If you do not understand TypeScript, you can do one of the two things:
- Learn TypeScript and write code as sane people do.
- For simple code snippets, remove the parts that are not part of the JS syntax. The resulting snippet is roughly the JS equivalent. For example,
Look at the code. At first glance, you’d expect the following:
- The first
setStatecall sets the value field of state to 10.
- Print “State is set to 10” to console.
setStatecall sets the value field of state to
state.value + 1which should be
10 + 1 = 11.
- Print the final state to the console which is 11.
But the program logs the following output:
State is set to 10
Also, the final
1 . You can check out the example for the same.
This is because all the
setState calls are async. They are batched and effectively applied (merged) after the method body executes. Inside the body of method,
this.state.value holds its original value
0 which is what is logged. When the two calls are combined:
state.valueis set to 10
state.valueis set to
state.value + 1which is
The correct way to do the above is by function arguments and callback functions.
The above snippet is written with consideration to the async nature of state update. No matter when the state is updated, we are guaranteed to have a consistent state (React guarantees that the
setState calls are considered in the order they are called). This time, the output is:
State is set to 10
11. The documentation mentioned that React may introduce more cases and features where
setState is async (this and this). Since this is simply an implementation detail, you should not rely on it and always consider
setState async. Finally, a rule of thumb is :
setStatecalls that are triggered inside a callback do not get batched with the calls outside the callback as they do not arise as a result of the same event.
The concurrent mode
React can now be run in concurrent mode. In this mode, it can work on several state updates concurrently. By concurrent, I mean that they will effectively take place together. It does not mean that state updates are executed in parallel.
Synchronous, asynchronous, concurrent and parallel
Consider the following analogy. You are doing your household chores. That includes doing the dishes, laundry and cooking your lunch. Imagine a case where you have two of your friends to help you out. One of you can do the dishes, the other one can do the laundry and the last one can make lunch (sounds like something that should be settled by a game of Rock — Paper — Scissors). This is an example of tasks being done in parallel (Since tasks are being done together, they are concurrent).
Now imagine that you are the only one in the house. You can do all those tasks one by one (synchronously). But that isn’t very efficient, is it? So you do them all at once. You pile the dishes and start up the dishwasher. While that happens, you collect the laundry and put it in the washing machine. While it works its magic, you start chopping veggies for lunch. You keep alternating between tasks till all of them are done. That is an example of tasks being done concurrently but not in parallel. When tasks are carried out concurrently and not in a sequence, they are said to be carried out asynchronously (in async). An important feature of concurrency is that tasks (chores) share a resource (human being) while they are performed together.
Finally, to sum it up here is a definition from Wikipedia:
In computer science, concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in a partial order, without affecting the outcome.
When tasks are performed in async, it is generally harder to keep track of what is happening and when. For an outsider, it is difficult to determine what chore you do and when you do it. Similarly, when
setState calls are treated as async, you assume that you don’t know when that state update will take place and hence write your code with the help of callback functions to maintain the order.
Concurrent state updates
Note: Since this is a new concept, you might have trouble understanding it in the first try. You can read this article over again. Moreover, as we progress into this series of articles, things will start to make more sense. You can then come back to this article.
Conventionally, when state was updated, it triggered a re-render (think to render and commit phases). Once that started, nothing else could be done until the re-render finished. That is blocking mode of rendering UI. So if a state update is taking place right now, next update has to wait till the current update finishes. A month ago, I was working on a form builder. The structure of the app was such that a single keypress caused the entire app to re-render. Typing a short phrase caused around 110 re-renders. That is undesirable, isn’t it?
In concurrent mode, React can take multiple state updates and work on them concurrently. It can render (think to render phase) the updates concurrently in memory. State updates (tasks) need DOM (resource) to finally commit the result of the render phase. To use the analogy from documentation, imagine that every update is done in a different universe. When that update is ready, it is merged into our universe.
If everything happens randomly, it can lead to inconsistency. You won’t know which update is rendered when. Anything can occur anytime and you can no longer determine the state of the application by prior analysis (by studying the code). Usually, a mechanism is required to implement concurrency (to avoid inconsistency). React assigns priorities to the state updates. That priority is based on a heuristic and can be adjusted with just a few lines of code. This is explored in the later articles. Moreover, the legacy version of React lacks consistent batching guarantees. In concurrent mode, all state updates are batched by default.
Take a moment. Take a step back. Think of what concurrent mode is. Think of all the possibilities that it can bring with itself. Can you write down a few? I personally see two possibilities:
- Enhanced performance: React is already very performant due to its diffing algorithm and other optimisations like batching state updates. But this can be a game-changer. The only major pain point for performance was waiting for renders to complete. But now with that gone, we can have much better performance.
- New UI Patterns: Since state updates take place concurrently and not one by one, you have the previous state and the new state (In legacy version, previous state is discarded when the new state is being prepared). That can help you build some interesting UI patterns.
When you look at React documentation, a good amount of discussion is centred around the above two. We will explore all of that in later articles.
Putting research into production
Note: This section is mostly a paraphrasing of the corresponding section of the documentation. It is mentioned just for the sake of completeness.
As per the React documentation, the concurrent mode is driven by their mission to integrate the findings from the Human-Computer interaction research into real UIs. There are a few examples of that.
- Their research shows that showing too many basic loading states can lead to poor user experience. Basic as in a simple spinner or a blank screen. But if the application has a lot of data fetching and state updates, it is almost inevitable. As the next state updates, you lose the previous state. But the new screen is not ready yet either. So you are stuck with a loader. Can you think of how concurrent mode solves this problem? (This is explored in further articles)
- Multiple updates can take place in an app. Some as a result of processing such a data fetching and others as a result of user interaction such as hover, click or typing in an input. Their research shows that if the interaction-based updates are processed as soon as possible and the others a bit delayed, user experience is not degraded much. But if interactions need to wait for other updates to finish, app feels laggy and jarring. Concurrent mode solves this with priority-based updates (We explore this too later).
Teams with focus on UX handle these problems with one-off solutions. But these are typical and do not last long. However, with concurrent mode and priorities, this comes as a part of React. It is nicely baked in and abstracted away so that you can use React as you do and you can use these features in a way natural to React.
You can already feel the awesomeness of it all, can’t you? This article was meant to give you a taste of what the concurrent mode is. You now know what new features the concurrent mode unlocks. Next article will explore concurrent mode from a more practical point of view. It will help you realise what concurrent mode means for you as a developer and how you can leverage it.
Did you enjoy this article? Did you spot a mistake? Did I miss something? Does something need more (or less) explanation? Could this article be better? Let me know in the comments ; )