Understanding React lazy, Suspense and Concurrent React with Examples

In this article we will be exploring the usage of React.Lazy, React.Suspense and other new features of React.

There are some major new features and enhancements coming up for the React framework; exciting for us developers, rewarding for the end user.

In this article we will be exploring the usage of lazy() and <Suspense />, but for these features to be fully beneficial we also need to understand concurrent React and the benefits of its support for multi threading.
Let’s start with what these terms mean:
  • Concurrent React: An update to the underlying React framework that allows it to work on multiple tasks (renders) at the same time. Not only this, these tasks can be switched between according on their priority, on a real-time basis.
  • lazy(): lazy is a new API in React to aid in code splitting and importing components into your scripts — very easily.
  • <Suspense />: Suspense is like an error catcher, which allows us to define fallback JSX if part of the content it is wrapping has not loaded. If you think of a try catch block, the catch block is our Suspence fallback, and everything within <Suspense></Suspense> is our try block. This example feels a bit contrived as catch blocks should generally not execute business logic, but the underlying process is very similar.

Suspense also lets us define a threshold to hold off showing a spinner in the event a component has not loaded in a certain time period. Think of using apps with a fast internet speed that flash in a spinner for a split second, before the loaded content is displayed. We can prevent this with fallbacks.
Use the following NPM command to install this version for react and react-dom:
npm install react@16.8.0 npm install react-dom@16.8.0
So the great news is that we can jump into playing with concurrent React straight away. Below we will explore how to use concurrent React, and jump into lazy() and <Suspense /> examples.

Note: Even though these features — and others announced at React Conf 18 — offer some great new ways to work with React, I would not recommend you jump into the alpha and replace your current codebase just yet. Alpha builds usually contain experimental or incomplete modules, which upon final release, may change in function or API. Instead, duplicate your existing projects for testing the concurrent behaviour of your App.

. . .

React.Lazy

As mentioned above, lazy() allows us to perform code splitting within our app and import components very easily.
(Code splitting — The idea of breaking (or splitting) your app into various parts so your entire app does not need to load straight away. You may want to only download the homepage for example, and later on download another part of your app when the user actually visits there. Or, since React is now asynchronous, you may want to load the content right before the user visits there!)
Let’s say I want to load a <Product /> interface, responsible for displaying a product page. This is not necessary when a user first visits my site, therefore I want to split this away from my main bundle.
Let’s go ahead and import this component:
import React, { lazy } from 'react'; const Product = lazy(() => import('./ProductHandler'));
lazy is imported from the standard React library. It is just a function. We also pass a function into lazy() as its only argument. This function must return a dynamic import, which returns a Promise to a component containing the React component we want to load (which needs to be the default export of the module). This is pretty minimised code, and is not a big departure from what we are used to

if we simply import a component without code-splitting capabilities:
import { Product } from './ProductHandler';

Note: Standard import statement with no code-splitting!
. . .

React.Suspense

Now we have a simple API for importing components via code splitting, we need a way to display them if they are fully loaded — and display a placeholder if they are not. This is where <Suspense /> comes into play.
Implementing Suspense is also straight forward, and does not require a big change to your JSX markup.
Import Suspense from the standard React package library with the following
import statement:
import React, { lazy, Suspense } from 'react';
To implement Suspense, wrap your JSX (with your lazily loaded imported component within it), with the <Suspense> object:
... render() { return( <div className='product-list'> <h1>My Awesome Product</h1> <Suspense fallback={<h2>Product list is loading...</h2>}> <p>Take a look at my product:</p> <section> <Product id='PDT-49-232' /> </section> </Suspense> </div> ); }
Here, we have wrapped our lazily loaded <Product /> within <Suspense>. Suspense’s fallback prop allows us to pass a component (or any JSX markup) into Suspense to display our loading state. In the example above I have just used a <h2> subheading, but feel free to use your a spinner.
I have deliberately placed some other JSX markup within Suspense to demonstrate that Suspense does not need to be the direct parent object of our lazily loaded components.
This is also advantageous to us as we may not want to display the “Take a look at my product:” text before <Product /> has loaded. Furthermore, <section>provides additional padding and styling that would look odd in an unloaded product state.
If my component has not loaded, Suspense works from that component, up the component tree until a Suspense object is found.
What we can also set up is a scenario where Suspense is waiting for mutliple lazily loaded components to resolve:
... render() { return( <div className='product-list'> <h1>My Awesome Product</h1> <Suspense
fallback={<h2>Product list is loading...</h2>}
> <p>Take a look at my product:</p> <section> <Product id='PDT-49-232' /> <Product id='PDT-50-233' /> <Product id='PDT-51-234' /> </section> </Suspense> </div> ); }
In this scenario, all my products need to load before Suspense moves from the fallback to my loaded content.
What is not well documented online is Error Boundaries. What are Error Boundaries?
Error Boundaries allow us to catch errors from anywhere in their component tree, log the error, and display fallback UI. We define ErrorBoundaries as a separate class component, define the magic methods getDerivedStateFromError, componentDidCatch and render, and simply import the component and include it in our render method.
In the case dynamic imports fail to load, Error Boundaries come in handy to display a more friendly UI (more-so than a never ending spinner). An Error Boundary could catch the network error triggered when the module fails to load, which in turn can render a UI that notifies the end user, and could provide a button to reload the resource (or trigger an automatic reload).
More information on Error Boundaries can be found on reactjs.org: What we talked about above can be implemented in synchronous React, but what if we wanted to take advantage of the asynchronous capabilities of concurrent React?

. . .

Concurrent React

We change the way we render our root <App /> element. Where we do this in standard React:
ReactDOM.render(<App />, document.getElementById('root'));

We do this for concurrent React:
ReactDOM.createRoot(document.getElementById('root')).render( <App /> );
That is all that needs to be changed to enable Concurrent React. To continue the talk about Suspense, one of the things it can do in the Concurrent environment is support loading spinner thresholds.
There is more to the story of Suspense. As mentioned above, we can also define a fallback to prevent the loading state displaying, in the event of a fast internet speed. Just include the <Suspense /> maxDuration prop, which takes a millisecond value.
Again, don’t expect this to work in non-concurrent React.
. . .

Enable Strict Mode

If you are developing in React 16.6, what has been recommended is to wrap <React.StrictMode> around <App /> so any unsupported features you may integrate will be prompted as warnings in your development console. Wrap strict mode around your app like so:
ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );

Suspense’s maxDuration threshold is just one advantage of using Concurrent React.
Concurrent React promises to be high performance and deliver smooth UX, which primarily entails less transitions when loading or fetching content. All this functionality is opt-in, meaning developers do not have to adopt any of it, and adopting it will not break your current codebase.
With lazy() and <Suspense />, we are only scratching the surface of what will materialise from an asynchronous React framework — we can certainly expect big changes to packages, apps and user experiences in the coming months.
The React core devs are also working on experimental packages such as react-cache, which has the ability to cache imported and fetched data, and therefore have no need to request it again upon every page visit or refresh. This is not available for public testing at this time, but may be another valuable tool for additional UI simplicity for the end user.

It's not the same without you

Join the community to find out what other Ednsquare users are discussing, debating and creating.