Yomi Eluwande JavaScript developer. Wannabe designer and Chief Procrastinator at Selar.co and worklogs.co.

React Suspense: Async rendering in React

An explanation of how and why to use React Suspense.

6 min read 1948

Async Rendering in React Suspense

Editor’s Note: This blog post was updated 30 August 2021 to include the latest information available regarding React Suspense. 

Suspense is a new React feature that was introduced in React 16.6. It aims to help with handling async operations by letting you wait for some code to load and declaratively specify a loading state (like a spinner) while waiting.

It allows you to defer rendering part of your application tree until some condition is met (for example, data from an endpoint or a resource is loaded). While this article will be focused on the data fetching application of Suspense, Suspense can also be used for fetching other resources like images, scripts, or other asynchronous work.

In this article, we’ll explore Suspense and see what potential impact this feature will have on the way React apps are built.

Before we go on in the article, note that React Suspense is an experimental feature and is still undergoing changes, especially with the upcoming release of React 18.

Why React Suspense?

There’s a good chance you’ve come across SPAs that make use of a loading icon as an indicator that data is being fetched. This is a common method used to ensure good UX for apps that are fetching data from external sources. All you have to do is check if the data has been successfully fetched, and if not, show a spinner.

However, this might not scale when the data fetching process becomes complicated:

  • When both the parent and child component have loading states
  • When you need a component to load only after some other (child) components have been loaded

To see Suspense in action, let’s compare the implementation of the classic conditional rendering method against Suspense.

I’ve gone ahead to build the classic conditional rendering method here in this GitHub repository and you can check it out. The codebase is for a React app that fetches data from TVMaze, a free public API that lets you search for information about TV shows.

The main file of interest here is the Shows/index.js file, which is responsible for fetching the required data and rendering it:

We made a custom demo for .
No really. Click here to check it out.

import axios from "axios";
import { useEffect, useState } from "react";
import * as Styles from "./styles";
const formatScore = (number) => {
 return Math.round(number * 100);
};
const Shows = () => {
 const [isLoaded, setIsLoaded] = useState(false);
 const [shows, setShows] = useState([]);

 useEffect(() => {
   axios(`https://api.tvmaze.com/search/shows?q=heist`)
     .then((r) => {
       console.log(r);
       setShows(r.data);
       setIsLoaded(true);
     })
     .catch((e) => {
       setIsLoaded(false);
       console.log(e);
     });
 }, []);

 return (
   <Styles.Root>
     {!isLoaded && <p>loading...</p>}
     {isLoaded && (
       <Styles.Container>
         {shows.map((show, index) => (
           <Styles.ShowWrapper key={index}>
             <Styles.ImageWrapper>
               <img
                 src={show.show.image ? show.show.image.original : ""}
                 alt="Show Poster"
               />
             </Styles.ImageWrapper>

             <Styles.TextWrapper>
               <Styles.Title>{show.show.name}</Styles.Title>
               <Styles.Subtitle>
                 Score: {formatScore(show.score)}
               </Styles.Subtitle>
               <Styles.Subtitle>Status: {show.show.status}</Styles.Subtitle>
               <Styles.Subtitle>
                 Network: {show.show.network ? show.show.network.name : "N/A"}
               </Styles.Subtitle>
             </Styles.TextWrapper>
           </Styles.ShowWrapper>
         ))}
       </Styles.Container>
     )}
   </Styles.Root>
 );
};

export default Shows;

As seen in the code block above, the component has a useEffect function that is used to fetch data from the TVMaze’s API and store it in a local state. Because we’d like to keep the app interactive and not let users see an empty screen, there is an isLoaded state and its value is a Boolean. isLoaded is used to show a loading indicator on the screen depending on whether the data has been successfully fetched.

This method is generally called “fetch-on-render” because fetching doesn’t start until the component has been rendered.

Now let’s see how this example would be rewritten using React Suspense. You can clone the existing repo here to start with.

Firstly, let’s ensure that the experimental version of React (alpha) is installed, as Suspense is still an experimental feature. You can do that by running the command below:

npm install [email protected] [email protected]

Let’s begin by adding the Suspense component to the React app. We need to place the required components inside the <Suspense /> component so that Suspense is aware of the components that need to be blocked until the needed data is available.

We can do that by editing the App.js file in the components directory with the code block below:

import React, { Suspense } from "react";
import "./App.css";

const Shows = React.lazy(() => import("./components/Shows"));

function App() {
 return (
   <div className="App">
     <header className="App-header">
       <h1 className="App-title">React Suspense Demo</h1>
     </header>
     <Suspense fallback={<p>loading...</p>}>
       <Shows />
     </Suspense>
   </div>
 );
}

export default App;

In the code block above, the Shows component is wrapped with a Suspense component which has a fallback prop. This means that while the component Shows is waiting for some asynchronous operation, such as fetching shows from TVMaze’s API, React will render <p>loading...</p> to the DOM instead. The Shows component is then rendered only after the promises and APIs are resolved.

Suspense itself is not a data-fetching library. Instead, it’s a mechanism for data-fetching libraries to communicate to React that the data a component is reading is not ready yet.

Currently, the way to use Suspense in conjunction with your data fetching library is by making use of a mechanism called Resources.

Resources are sources of async data for Suspense. A resource is simply an object that returns a read method. The read method will either return the result of the async data or an error. React Suspense will then call that method as needed for its inner workings.

Next, let’s create the file that houses the resource for this example. Create a new file, fetchShows.js, in the components directory. Edit it with the code block below:

import axios from "axios";

export const fetchShows = () => {
 let status = "pending";
 let result;
 let suspender = axios(`https://api.tvmaze.com/search/shows?q=heist`).then(
   (r) => {
     status = "success";
     result = r.data;
   },
   (e) => {
     status = "error";
     result = e;
   }
 );
 return {
   read() {
     if (status === "pending") {
       throw suspender;
     } else if (status === "error") {
       throw result;
     } else if (status === "success") {
       return result;
     }
   },
 };
};

In the code block above, the fetchShows function returns the read object, which is the resource needed for Suspense. There are some variables at the top of the function; the result variable will hold the response from the API request and status, which contains the current status of the API request and could be pending, success, or error.

There’s also a suspender variable that stores the API request itself. After the server responds, the status and result variables are updated. Here’s how the read object works:

After setting the variables status and result to their appropriate values in the .then() function of the API, if the status is pending, the read object throws the suspender promise itself, which Suspense will catch and display the fallback component.

If the status is completed, the read object returns the result, and if the status is error, the read object throws the result, which, in that case, is an error instance.

Next, let’s see how the resource can be used in the Shows component. Edit theShows/index.js file with the code block below:

import { fetchShows } from "../fetchShows";
import * as Styles from "./styles";

const resource = fetchShows();

const formatScore = (number) => {
 return Math.round(number * 100);
};

const Shows = () => {
 const shows = resource.read();

 return (
   <Styles.Root>
     <Styles.Container>
       {shows.map((show, index) => (
         <Styles.ShowWrapper key={index}>
           <Styles.ImageWrapper>
             <img
               src={show.show.image ? show.show.image.original : ""}
               alt="Show Poster"
             />
           </Styles.ImageWrapper>

           <Styles.TextWrapper>
             <Styles.Title>{show.show.name}</Styles.Title>
             <Styles.Subtitle>
               Score: {formatScore(show.score)}
             </Styles.Subtitle>
             <Styles.Subtitle>Status: {show.show.status}</Styles.Subtitle>
             <Styles.Subtitle>
               Network: {show.show.network ? show.show.network.name : "N/A"}
             </Styles.Subtitle>
           </Styles.TextWrapper>
         </Styles.ShowWrapper>
       ))}
     </Styles.Container>
   </Styles.Root>
 );
};

export default Shows;

In the code block above, the resource is imported from fetchShows and initialized as the resource variable.

We do not need to check for null or present a loading indicator, but rather, just call resource.read() without any safe checking. Even though fetching the shows from TVMaze’s API is an async process, the resource is used synchronously because Suspense takes care of fetching and checking the data.

This approach makes our application cleaner and more robust by eliminating the need for safe checking, conditional rendering, preventing race conditions.

React Suspense ErrorBoundary

One more thing to consider for Suspense is how it handles errors. We throw an error in the resource file fetchShows.js but we don’t do anything to notify the user that some error has occurred.

React provides error boundaries to help with this. Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Since we throw an error if there is one in the resources file, that means we can create an ErrorBoundary component and embed it in our component tree.

So let’s create the error boundary component to use across our project. Create a file ErrorBoundary.js in the src/components directory:

import React from "react";

// Error boundaries currently have to be classes.
class ErrorBoundary extends React.Component {
 state = { hasError: false, error: null };
 static getDerivedStateFromError(error) {
   return {
     hasError: true,
     error,
   };
 }
 render() {
   if (this.state.hasError) {
     return this.props.fallback;
   }
   return this.props.children;
 }
}

export default ErrorBoundary;

The ErrorBoundary component can then be used in the App.js file like this:

import React, { Suspense } from "react";

import ErrorBoundary from "./components/ErrorBoundary";
import "./App.css";

const Shows = React.lazy(() => import("./components/Shows"));

function App() {
 return (
   <div className="App">
     <header className="App-header">
       <h1 className="App-title">React Suspense Demo</h1>
     </header>
     <ErrorBoundary fallback={<p>Could not fetch TV shows.</p>}>
       <Suspense fallback={<p>loading...</p>}>
         <Shows />
       </Suspense>
     </ErrorBoundary>
   </div>
 );
}

export default App;

You can test that this works by editing the API URL in the fetchShows file to be invalid and the ErrorBoundary component will be displayed.

Conclusion

With Suspense, you have the ability to suspend component rendering while async data is being loaded. You can pause any state update until the data is ready, and you can add async loading to any component deep in the tree without plumbing all the props and state through your app and hoisting the logic.

This results in an instantaneous and fluid UI for fast networks and an intentionally designed loading state for slow networks as opposed to a general loading state.

It’s important to note that these APIs are still in experimental mode and not suitable for production. It’s best to always stay in tune with the React team for any API changes and updates to the Suspense feature.

The codebase for the Suspense demo above can be accessed on GitHub and a live demo can be seen on CodeSandbox.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Yomi Eluwande JavaScript developer. Wannabe designer and Chief Procrastinator at Selar.co and worklogs.co.

Leave a Reply