React lazy, Suspense and Concurrent React Breakdown with Examples

Exploring lazy() and <Suspense /> within the context of concurrent 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.

Can we use these features now?

lazy() and <Suspense />: Yes — 16.6

These 2 features are available to integrate right now, and were released with the React 16.6 update. However with 16.6, we can only use them in standard synchronous React. Concurrent React is not yet included in a final public release, but is available in the 16.7 alpha build.

Concurrent mode: Yes — 16.7 alpha

As mentioned above, we need to build React16.7 alpha in order to test and use React in concurrent mode.

This is not problem for testing purposes; use the following NPM command to install this version, for react and react-dom:

npm install react@16.7.0-alpha.0
npm install react-dom@16.7.0-alpha.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.

A lazy() Example

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!

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.

Using <Suspense /> to manage lazy() importing

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.

Suspense is not Component Tree sensitive

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.

Suspense works up the Component Tree from the lazily loaded component

If my component has not loaded, Suspense works from that component, up the component tree until a Suspense object is found.

This means we can have nested Suspense objects within other Suspense objects. Consider the following nest diagram:

MainInterface
  Suspense  
lazy ProductComponent
Suspense
lazy ProductImageGalleryComponent
Suspense
lazy ProductReviewsComponent

In the following scenario I will also lazily load my product images interface and product reviews interface. The latter 2 are totally independent of each other, and are initiated once my ProductComponent has loaded.

When does this make sense to use? If 2 criteria are met:

  • When the images and reviews interfaces contain a bulk of isolated code that the user may not visit. If a low percentage of users actually visit these components, it is not worth loading them on the initial app load.
  • If the image gallery and reviews components are used elsewhere in my app. I am already lazy loading my product page anyway, so loading more components within that page, that are only used within my product page, may not make a lot of sense. Of course this is all relative to file sizes of the components.

We can also wrap multiple lazily loaded components under one Suspense object

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.

Also Worth Noting: <ErrorBoundary />

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:

How to use Concurrent React: ≥16.7 alpha only

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?

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.

Suspense Fallback Threshold: maxDuration prop

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.

Note: If you are implementing Suspense now with React 16.6, hold off until 16.7 is in final release before supporting thresholds. To be sure you are not integrating anything not supported by your current version, run your development build with React.StrictMode.

React.StrictMode

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'));

Conclusion

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.