In a recent post, we discussed building great user experiences using React Suspense. Continuing on that post, we will have a look at how React’s new experimental Concurrent mode helps us develop responsive and fast apps.

When it comes to great UX, performance is one of the key factors. While working on large scale applications we often encounter situations where we have to compromise user experience to keep our apps fast. But what if we could find a way to build apps that are highly performant by default with smooth UX and developer-friendly APIs? React Concurrent Mode is the answer to all of these wishes.

Concurrent React is something that has been under development at Facebook for the past 2 years now. React Fiber, a complete rewrite of React, was done with concurrent mode as central motivation. Though all of the other benefits of React Fiber are worth it themselves but enabling Interruptible Rendering and concurrency is the best amongst them.

React Concurrent Mode - A Theory

What is concurrent React?! As React Docs mention:

Concurrent Mode is a set of new features that help React apps stay responsive and gracefully adjust to the user’s device capabilities and network speed.

Diving deep, Concurrent React can work on multiple tasks at a time and can switch between them depending upon the priority.

The way UI libraries work today is the blocking render approach: when the library has started rendering it cannot go back, no matter how important the next update is. For example, once the browser has started rendering a list of search results, any change in the search input field has to wait until this rendering is finished.

This is not the case anymore with concurrent mode. React starts rendering updates in memory and high priority tasks such as a change in an input field can interrupt that rendering process. So when there is a change, React will let the browser paint the change and the rendering process will continue once this high priority task is completed.

Afterward, upon completion of list rendering, React will commit it to DOM and also be displayed on the screen. In short, With Concurrent React, you can render large amounts of work without blocking user inputs and other high priority tasks on the main thread.

Concurrent React can partially render a tree without committing the result to the DOM. For example, React starts rendering an update and it encounters a component that hasn’t finished loading. React will wait for loading to be finished without necessarily rendering some fallback component.

One of the best use cases to demonstrate this will be navigation between different pages. Once a user taps on a link and code to display the component, an appropriate loading state is not available. Here React can wait for a little and keep showing the current screen fully interactive to avoid bad loading state. Once sufficient data or code is available this transition can be completed. Though it can be done via different techniques Concurrent mode makes it more intuitive for developers.

All of this is done by just a few lines of code as React uses heuristics to decide how urgent updates are.

Enabling React Concurrent Mode

As we discussed in our previous post, the developer experience has been at the core of React’s growth. The same goes for Concurrent React, as it is super easy to adopt. Either you’re relying on class components or cruising with hooks, Concurrent React will support both.

Though Concurrent React is used in production at Facebook, for open source users its available in an experimental version of React. To install the experimental version you can use the following command

npm install react@experimental react-dom@experimental

Unlike other features that don’t require any changes in the existing code base, Concurrent mode requires a little change on the root of the React component tree. Here is that small change

import ReactDOM from 'react-dom'; // Current React - Or Blocking Rendering React: // // ReactDOM.render(<App />, document.getElementById('root')); // // Concurrent Mode - Or Interruptible Rendering React: ReactDOM.createRoot( document.getElementById('root') ).render(<App />);

One important notice here, the lifecycle methods that were marked as UNSAFE_ in previous React releases are truly unsafe with Concurrent Mode. It is highly recommended that you move to new lifecycle methods for a secure transition to Concurrent Mode.

Understanding APIs

In order to fully utilize Concurrent Mode and develop highly responsive interfaces, React gives developers an intuitive API to handle the transition between states and loading states.

useTransition

As discussed above unlike blocking rendering where all updates are treated with the same priority, Concurrent Mode allows developers to de-prioritize slower updates to keep interfaces interactive.

The useTransition hook allows developers to schedule work after higher priority work.

Continuing with an example of showing search results under input, updates in the input are of highest priority to keep UI interactable. However, fetching data and rendering lists is of low priority. Wrapping our update in startTransition communicates to React that these updates are of lower priority and can be scheduled after high priority tasks.

A number of research about handling loading states suggests that gestures like hover and input change should be handled at the highest priority to avoid laggy outlook of the interface. However, a little bit of wait on click and during page transitions is bearable for users. Supporting this research useTransition hook allows developers to wait for a time period before displaying any loading state.

Here is full API of useTransition hook:

const [startTransition, isPending] = useTransition({ timeoutMs: 2000, });

startTransition - Method to wrap low priority updates

isPending - Boolean indicating if there is any pending low priority update, likely used for showing loading state.

timeoutMs - Milliseconds after the update is marked as pending.

useDeferredValue

React Concurrent Mode allows developers to produce the magic of useTransition themselves. With useTransition we get isPending available right away, whereas useDefferedValue gives us the previous value of state that is in transition. API looks like the following:

const deferredValue = useDeferredValue(value, { timeoutMs: 2000 });

deferredValue will have a previous version of the value for at most timeout milliseconds.

This is commonly used to keep the interface responsive when you have something that renders immediately based on user input and something that needs to wait for a data fetch.

SuspenseList

SuspenseList is a component that wraps a bunch of child Suspense components and gives you handles for how to show all of the various loading states.

In apps that render a number of suspense components together, SuspenseList provides the ability to handle how these components are revealed to the user. Let's take an example to render a list of Blog posts on our page. List Component of our page will look like the following.

<SuspenseList revealOrder="forwards"> <Suspense fallback={<Loader />}> <BlogPostItem id={1} /> </Suspense> <Suspense fallback={<Loader />}> <BlogPostItem id={2} /> </Suspense> <Suspense fallback={<Loader />}> <BlogPostItem id={3} /> </Suspense> ... </SuspenseList>

SuspenseList takes two props:

revealOrder : [forwards | backwards | together] It defines the order in which the children Suspense components should be revealed. together : Quite similar to the Promise.all API, it renders complete list once all items are available forwards/backwards : This will reveal in sequence one after another, top-down or bottom, even if the component after next is ready it will keep it suspended until the next component is revealed.



tail: [collapsed | hidden] It determines how suspended items in the list are shown. By default React will display fallback for all items in the list. collapsed : It will show only the fallback of the next item in the list hidden : It will hide all fallbacks in the list



Conclusion

As we see today, React Concurrent Mode will allow front-end engineers to develop fast, reliable and scalable interfaces easily, paving the way for higher standards in user experience.