Concurrent React Mode: Using Suspense and useTransition to Build A Better UX

1,537 reads

@ agzuniverse Aswin G CS undergrad student with a love for tech. Currently hacking with JavaScript, Python and Golang.

The React JS dev team announced some exciting changes several months ago - React would be getting a "Concurrent Mode". Essentially this would allow React to perform multiple UI renders concurrently. Of course, JavaScript is single threaded and true concurrency is an illusion, but the new features will allow web apps (and Native Apps once these features hit React Native) to be much more responsive and snappy than they are now with less effort and custom code from the developer to make this happen.

reactions

Concurrent mode is now available in the experimental build of React, so let's dig in and see how to use the shiny new API.

reactions

In this post I'll be showing you how to use the

Suspense

useTransition

useDeferredValue

reactions

But what about the existing Suspense feature?

API with thehook. There is yet another hook,, that serves a slightly different, but equally important purpose that I'll cover in a follow up post.

You'll notice that the

Suspense

React.lazy()

reactions

The New Way - Render as you fetch

API has been present in React since v16.6. This is in fact, the same API that is being extended to do more in the React experimental build. In React 16.6, Suspense can only be used for one purpose: code splitting and lazily loading components using

This has been discussed a lot through talks and blogs and in the official documentation already, so I'll keep it brief - Concurrent React allows us to implement a "render as you fetch" pattern, which renders components as the data needed to populate them are fetched concurrently. React renders as much as it can without the available data, and renders the component that requires the fetched data as soon as the data becomes available. During this time these components are said to be "suspended".

reactions

The commonly used existing approaches are "fetch then render", which fetches all the data needed first before rendering the component, and "render then fetch" which renders a component and then the component itself fetches the data required to populate it's children. Both these approaches are slower and have a number of disadvantages that needed workarounds.

reactions

The Setup

For this, I'm using a basic React app I configured manually with Webpack and Babel (Click here for a guide I wrote on how to do that), with the only difference being running:

reactions

npm i react@experimental react-dom@experimental --save

To get the experimental versions instead of installing the release versions of

react

react-dom

reactions

and

This should also work with React apps created with

create-react-app

react

react-dom

reactions

Opting in to Concurrent Mode

by replacingandwith their experimental versions.

Since concurrent mode changes how React handles components fundamentally, you'll need to change the

ReactDOM.render()

index.js

reactions

ReactDOM.createRoot( document .getElementById( 'root' )).render( < App /> );

line in yourto:

This enables concurrent mode in your app.

reactions

I've also set up my

App.js

Data

reactions

import React from 'react' ; import Data from './Data' ; const App = () => { return ( < div > <p>React Concurrent Mode testing</p> <Data /> </ div > ); } export default App;

The Demo

to render a component calledinside which the demo is done.

Now we create

Data.js

reactions

import React, { useState, useTransition, Suspense } from 'react' ; import DataDisplay from './DataDisplay' ; import { dataFetcher } from './api' ; const initialData = { read : () => { return { foo : "initial" } } }; const Data = () => { const [data, setData] = useState(initialData); const [count, setCount] = useState( 0 ); const [startDataTransition, isDataPending] = useTransition({ timeoutMs : 2000 }); const fetchNewData = () => { startDataTransition( () => { setData(dataFetcher()) }) } return ( < div > <Suspense fallback={<p>Loading...</p>}> <DataDisplay data={data} /> <button disabled={isDataPending} onClick={() => fetchNewData()}>Click me to begin data fetch</button> </Suspense> <p>Counter: {count}</p> <button onClick={() => { setCount(count + 1); }}> Click me to check if the app is still responsive</button> </div> ) } export default Data;

Breaking this down:

reactions

dataFetcher

reactions

is a function that returns a "special" object that lets React know the states set as this object can be fetched as the components dependent on this state is rendered. These components are "suspended" if the data has not finished fetching. We'll look at how to create the "special object" towards the end.

initialData

dataFetcher

read

initialData

{ foo : "initial" }

reactions

shows the format of the object returned byonce the data has finished loading. It has afunction that returns the object with data we need. Ideally, theshould implement some sort of caching function for the last loaded data, but here we just use

A state which while being updated/fetched causes a component to suspend, must be updated using the

useTransition

reactions

hook. This hook returns a pair of values - a function that takes a callback function in which you set the state, and a boolean that lets us know when the transition is taking place.

The argument passed to

useTransition

reactions

is an object that tells React how long to wait before suspending the component. To understand it, think of it this way: We have some data on screen, and we're fetching some new data to replace it. We want to show a spinner while the new data is being fetched, but it's okay for the user to see the old data for a second, or maybe half a second before the spinner is shown. This delay is mentioned in this object.

This is useful in cases when showing stale data till new data is loaded is desirable, and also to prevent the spinner from showing up for a fraction of a second (causing what is percieved as jitter) on fast data fetch operations.

reactions

Let's take a closer look at

Suspense

reactions

<Suspense fallback={<p>Loading...< /p>}> <DataDisplay data={data} / > < button disabled = {isDataPending} onClick = {() => fetchNewData()}>Click me to begin data fetch </ button > < /Suspense>

itself:

Any component that should be suspended is wrapped inside the

Suspense

fallback

reactions

component. Inside it'sprop, we pass the component that should be shown instead while the component inside is waiting for data. This is usually a spinner or loading indicator of some sort to visually indicate to the user something is happening, so it doesn't appear as if the page hasn't responded to the click.

Here I've used the

isDataPending

reactions

boolean to disable the button while data is being fetched, preventing the user from pressing the button multiple times and sending multiple requests - A nice bonus we get from the pattern.

Talking about the page remaining responsive - it does. All the JavaScript in the page continues to work while the component is suspended and data is being fetched. The counter and the button to increment it can be used to confirm this.

reactions

DataDisplay

reactions

import React, { memo } from 'react' ; const DataDisplay = ( { data } ) => { return ( < h3 > {data.read().foo} </ h3 > ) } export default memo(DataDisplay);

is a simple component that takes the data and calls it's read function and displays the result.

memo

reactions

is used here to prevent this component from re-rendering when it's parent re-renders, and is essential for concurrent mode to work.

Finally, we look at

dataFetcher

api.js

reactions

export const dataFetcher = ( params ) => { return wrapPromise(fetchData(params)) } const wrapPromise = ( promise ) => { let status = "pending" ; let result; let suspender = promise.then( r => { status = "success" ; result = r; }, e => { status = "error" ; result = e; } ); return { read() { if (status === "pending" ) { throw suspender; } else if (status === "error" ) { throw result; } else if (status === "success" ) { return result; } } }; } const fetchData = ( params ) => { // In a real situation, use params to fetch the data required. return new Promise ( resolve => { setTimeout( () => { resolve({ foo : 'bar' }) }, 3000 ); }); }

and the other things inside

As you can see,

dataFetcher

wrapPromise(fetchData())

fetchData()

fetch()

fetchData()

params

setTimout

Promise

{ foo : 'bar' }

reactions

simply returns, andis a function that makes the actual request for the data. In a real situation you'll be usinginsidewith thepassed to it, or load data from some place else. Here I'm usingto return aobject that intentionally introduces a 3 second delay before returning

wrapPromise

Promise

reactions

is responsible for getting things to integrate with React, and should be straightforward if you've used Promises before. It returns the result if the fetch was successful, throws the error if it was not, and throws awith "pending" state if the operation has not completed yet.

All of this put together results in this:

reactions

Initially the data shown is "Initial". Then I click the button to begin data fetch. According to our configuration, the button is disabled immediately and nothing happens for 2 seconds. Then the component suspends, showing the fallback "Loading...". Then finally the data fetching is completed and the component updates to show "bar". During this whole time the remaining app (shown by the counter here) remains active.

reactions

As an ending note,

Suspense

useTransition

reactions

Conclusion

can work without thehook, but only if the required data is not part of the state.

It's worth saying here that the oddly specific functions in

api.js

reactions

is not too important, as it is expected that many popular data fetching libraries will support them in the future. React also does not recommend using concurrent mode in production because, well, it's still "experimental".

But it is expected that soon leveraging concurrent mode will be the de-facto way to get around lengthy operations in UI, allowing the creation of user experiences that remain consistent across many devices with vastly different processing powers and network connectivity speeds.

reactions

Tags