Linguine Code

Understanding how React Suspense works

I previously wrote an article about how to code splitting a React app at the router level.

And I received a comment, to write about React Suspense in detail.

Well that’s exactly what this article is going to be about.

What is React Suspense currently

React Suspense is a React component that suspends a component(‘s) being render until a certain condition has been met, and will display a fallback option.

This fallback option is required, and it may be a string or another React component such as a spinner.

For example, React Suspense only works with dynamic importing, aka lazy loading.


import React from 'react';

// Dynamic import()
const CatAvatar = React.lazy(() => import('./path/to/cat/avatar'));

// Displays "loading..." to the user until CatAvatar
// has finished loading.
const AppContainer = () => (
  <React.Suspense fallback="loading...">
    <CatAvatar />
  </React.Suspense>
);

In a previous article, I demonstrate how to implement React.lazy & React.Suspense to lazy load React routers.

Why is React Suspense useful?

The biggest problem with JavaScript applications now in days, it the hefty payload users have to pay to download & execute the code.

This is extremely expensive to users with weak devices, and network connections.

This is why code splitting JavaScript application is extremely useful.

React.lazy makes it really simple to tell Webpack and our application that certain files, and code can be loaded later in the application.

This will help reduce the initial size of code being shipped to the user.

But with that comes another problem.

As a user is navigating through the JavaScript application, and the code is being loaded on runtime, the user has to experience a delay until the network has finished loading and executing the next chunk of JavaScript code.

This is where React.Suspense comes in handy, and displays a graceful loading state to the user.

React.Suspense lets your users know that it’s loading the next chunk, will be with you shortly!

React can handle multiple React Components

If you want a full guide how to implement multiple components inside React.Suspense, check out this article.

But here’s a quick code snippet that shows you how to do this.


const CatAvatar = React.lazy(() => import('./path/to/cat/avatar'));
const ThorAvatar = React.lazy(() => import('./path/to/cat/thor-avatar'));

const AppContainer = () => (
  <React.Suspense fallback="loading...">
    <CatAvatar />
    <ThorAvatar />
  </React.Suspense>
);

You can have multiple nested React.lazy components inside React.Suspense.

Handling errors with React.Suspense & React.lazy

A couple issues may occur with React.lazy & React.Suspense.

  • The users network gave out in the middle of a network fetch
  • Not enough strength in the device
  • Incorrect path name to React component file

So how do we handle loading failures?

React has a standard pattern to handle errors gracefully. They have exposed 2 React lifecycles, static getDerivedStateFromError() and componentDidCatch().

These two lifecycles are similar in the way that they both get triggered when an error has occurred from child component.

The difference is:

static getDerivedStateFromError() requires you to return an object to update state.

As for componentDidCatch() it’s a void method, and you’ll have to call useState() or setState() to update the component state.

I’m going to add to the React.Suspense example above.


import React from 'react';

const CatAvatar = React.lazy(() => import('./path/to/cat/avatar'));

class ErrorHandler extends React.Component {

  state = {
    hasError: false,
  };

  static getDerivedStateFromError(err) {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return (
        <p>
          Loading has failed. Try refreshing the browser!
        </p>
      );
    }

    return this.props.children;
  }
}

const AppContainer = () => (
  <ErrorHandler>
    <React.Suspense fallback="loading...">
      <CatAvatar />
    </React.Suspense>
  </ErrorHandler>
);

If an error has occurred in loading the cat avatar, the error message will display.

React Suspense for data fetching

As demonstrated above, React Suspense only works with dynamic import(). It’s only meant to support lazy loading.

But at the moment, the React team is working hard to make React.Suspense work with data fetching as well.

React.Suspense ties in with the new mode that React is working on called, Concurrent Mode.

Concurrent mode provides a new set of features that tackles the original problem that was described earlier in this article.

That problem is weak devices, and bad network connections. As engineers we want to make sure we provide the same fast, and reliable applications to all users.

With Concurrent Mode turned on this opens a new set of features for React.Suspense.

First, to enable Concurrent Mode, you must install the experimental version of React.


npm install --save react@0.0.0-experimental-5faf377df react-dom@0.0.0-experimental-5faf377df

The second step is to replace how we attach the React application to the HTML shell.


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

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

That’s it! All you have to do is install the experimental mode of React, and change 1 line of code.

Then, React.Suspense will be default not only wait for dynamic import() but also data fetching.

React.SuspenseList – API

Another new feature that has been added to Concurrent Mode is React SuspenseList.

This is a React component that accepts multiple React.Suspense components and lets you orchestrate how they should be revealed.


// Cat 1 fetch time: 45ms
// Cat 2 fetch time: 100ms
// Cat 3 fetch time: 10ms

// Displays all children when all the data fetching
// has been completed
<React.SuspenseList revealOrder="together">
  <React.Suspense fallback={'Loading first cat...'}>
    <Cat id={1} />
  </React.Suspense>
  <React.Suspense fallback={'Loading second cat...'}>
    <Cat id={2} />
  </React.Suspense>
  <React.Suspense fallback={'Loading third cat...'}>
    <Cat id={3} />
  </React.Suspense>
</React.SuspenseList>

// Always starts from top to bottom: Cat 1, Cat 2, Cat 3
<React.SuspenseList revealOrder="forward">
  <React.Suspense fallback={'Loading first cat...'}>
    <Cat id={1} />
  </React.Suspense>
  <React.Suspense fallback={'Loading second cat...'}>
    <Cat id={2} />
  </React.Suspense>
  <React.Suspense fallback={'Loading third cat...'}>
    <Cat id={3} />
  </React.Suspense>
</React.SuspenseList>

// Always starts from bottom to top: Cat 3, Cat 2, Cat 1
<React.SuspenseList revealOrder="backwards">
  <React.Suspense fallback={'Loading first cat...'}>
    <Cat id={1} />
  </React.Suspense>
  <React.Suspense fallback={'Loading second cat...'}>
    <Cat id={2} />
  </React.Suspense>
  <React.Suspense fallback={'Loading third cat...'}>
    <Cat id={3} />
  </React.Suspense>
</React.SuspenseList>

React.SuspenseList only works with the closest React.Suspense components. No more than one level deep.

Conclusion

At the moment React.Suspense works only with dynamic import() aka lazy loading.

If you’re not sure when to use, follow this guide:

  • Start at the router level. Check out this guide that shows you how to implement React.Suspense and React.lazy() to add lazy loading to React router.
  • Only split chunks of React applications that are not super critical to the user.

In the near future of React, React.Suspense will not only be used for lazy loading but also for data fetching.