DEV Community

Cover image for Async rendering in React: Suspense, Hooks, and other methods
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Async rendering in React: Suspense, Hooks, and other methods

Written by Yomi Eluwande✏️

Async operations are common in modern web applications. Fetching data from an API, loading large components, or running computational tasks are all examples of asynchronous code that take some time to complete. In React, rendering components asynchronously can improve perceived performance by allowing certain parts of the UI to render immediately, while other parts wait on async operations.

React 18 introduced a powerful new feature called Suspense that allows components to "suspend" rendering while async logic is pending. When used correctly, Suspense enables coordinated asynchronous rendering across the component tree.

Before diving into how React Suspense works, let's first understand the basics of functional components and how to make them asynchronous using React Suspense.

Understanding async React components

Functional components have become the cornerstone of React development. They are JavaScript functions that return JSX, defining what should be rendered on the screen. They are concise, easy to test, and have gained immense popularity due to the introduction of Hooks.

React Suspense provides a straightforward way to make functional components asynchronous. It allows you to define which parts of a component should suspend rendering until async operations, like data fetching, are complete. This ensures that your UI remains responsive and that the user experience is seamless.

In functional components, data fetching and other async logic are usually performed inside the useEffect Hook. The problem is that this method blocks the entire component tree, even sections that don't depend on the async data.

React Suspense provides a straightforward way to fix this, thereby making functional components asynchronous. It allows you to define which parts of a component should suspend rendering until async operations are complete. This ensures that your UI remains responsive and that the user experience is seamless.

Implementing React Suspense for async rendering

Now that we understand the basics of functional components, async rendering, and Suspense, let's dive into the practical implementation of React Suspense for async rendering.

To use React Suspense, you need to set it up in your project. This includes importing the necessary components and wrapping your application in a Suspense boundary. This setup step is crucial for making async rendering work seamlessly:

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

Let’s see how Suspense is set up in a React project. Install the latest version of React using the command below:

npm install react@latest react-dom@latest
Enter fullscreen mode Exit fullscreen mode

The latest stable version of React (18.2.0) features the Suspense component, which has the following props:

  • children: The intended UI that you wish to display will render. If any child components pause during rendering, the Suspense boundary will transition to displaying the fallback content
  • fallback: An alternative user interface that can be displayed instead of the primary UI when it is not yet fully loaded. Any valid React node can be used as a fallback, but typically, it is a minimal placeholder view, such as a loading spinner or skeleton screen. When child components pause during rendering, Suspense will automatically switch to displaying the fallback UI, and revert to the child components once the required data is available

Let’s start with a basic example of using Suspense to fetch a list of TV shows from an API. For the purposes of this example, here’s a GitHub repository containing the source code.

We’re interested in three files in this codebase:

  • src/components/Shows/index.js
  • src/components/fetchShows.js
  • src/App.js

Let’s start with the src/components/Shows/index.js file:

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;
Enter fullscreen mode Exit fullscreen mode

Inside the Shows component, the fetchShows function is called to create a resource object that is used to fetch the list of TV shows. We wrote about the resource object and how it works in a previous article here.

Back to the Shows component — we use the resource.read() method to fetch the list of TV shows and assign it to the shows variable, which is then iterated over and used to display all the fetched shows in JSX.

Finally, in the App.js file, to asynchronously render the list of TV shows, we wrap the Shows component in the Suspense element with a fallback that displays loading...:

import React, { Suspense } from "react";
import "./App.css";
import Shows from "./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;
Enter fullscreen mode Exit fullscreen mode

Essentially, the Shows component is being suspended while the TV shows are being fetched. React utilizes the nearest Suspense boundary to display the fallback, which is ideally a loading component (in our case, a simple loading… text) until it is prepared to render.

After the data has been loaded, React conceals the loading fallback and proceeds to render the Shows component with the retrieved data.

Using Suspense to reveal content all at once

One of the key benefits of React Suspense is the ability to reveal content all at once when all async operations are complete. This eliminates the waterfall effect where parts of the UI load gradually. Instead, the entire component becomes visible only when it's ready.

Let’s have a look at how this can be implemented with an example. Again, this is available on the repository at the content-together-at-once branch here.

Using the previous example as a starting position, the src/components/fetchShows.js file has been renamed to src/components/fetchData.js and now looks like this:

import axios from "axios";

export const fetchData = (apiURL, artificialDelay) => {
  let status = "pending";
  let result;
  let suspender = new Promise((resolve, reject) => {
    setTimeout(() => {
      axios(apiURL)
        .then((r) => {
          status = "success";
          result = r.data;
          resolve();
        })
        .catch((e) => {
          status = "error";
          result = e;
          reject();
        });
    }, artificialDelay);
  });
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

This has now been re-purposed from exclusively fetching TV shows to fetching any kind of data as we now accept the API URL as an argument. We also have a new argument: artificialDelay, which is the amount of time in milliseconds that the function will wait before making the request. This will be useful later for adding an artificial delay to fetching external data from APIs.

In the previous example, we used TV Maze’s API to search for shows that contain heist in their title. Now for this example, which will involve fetching multiple content, we’ll fetch the details about a particular show (Money Heist, or La Casa de Papel) in one component and all of its episodes in another component. Then, we’ll place both components inside the Suspense boundary and see how Suspense can be used to reveal content all at once when all async operations are complete.

The src/components/ShowDetails/index.js file contains the ShowDetails component, which is used to fetch details about the show. The src/components/ShowEpisodes/index.js file contains the ShowEpisodes component, which is used to fetch all of the show’s episodes.

Additionally, the ShowEpisodes component has been set up in a way that there is a five second delay in addition to the time it takes for the API request to be completed:

const resource = fetchData(`https://api.tvmaze.com/shows/27436/episodes`, 5000);
Enter fullscreen mode Exit fullscreen mode

The App.js file also now looks like this:

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

import ShowDetails from "./components/ShowDetails";
import ShowEpisodes from "./components/ShowEpisodes";

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

export default App;
Enter fullscreen mode Exit fullscreen mode

With this, the whole tree inside Suspense is treated as a single unit. So in our case, even though the ShowDetails component will be done loading before ShowEpisodes, the loading indicator will still be displayed until all components inside the Suspense boundary are done with their operations: React Suspense Demo Loading Show Information All At Once

Using Suspense to reveal nested content as it loads

React Suspense isn't limited to top-level components. You can use it to reveal nested content as it loads, ensuring that your UI remains responsive and the user is never left waiting. Let’s have a look at how this can be implemented with an example. Again, this is available on the repository at the nested-content branch here.

We’ll be working with the ShowDetails and ShowEpisodes components again but with a few changes to demonstrate how Suspense can be used to reveal nested components as it loads.

The first change is in the ShowDetails component, where we now directly import and utilize the ShowEpisodes component as opposed to the previous example where it was being utilized in the App.js file:

import React, { Suspense } from "react";
import { fetchData } from "../fetchData";
import * as Styles from "./styles";
import ShowEpisodes from "../ShowEpisodes";
const resource = fetchData(`https://api.tvmaze.com/shows/27436`);

const removeTags = (str) => {
  if (str === null || str === "") return false;
  else str = str.toString();
  return str.replace(/(<([^>]+)>)/gi, "");
};

const Loading = ({ name }) => (
  <Styles.Loading>
    <p>loading episodes for {name}...</p>
  </Styles.Loading>
);

const ShowDetails = () => {
  const show = resource.read();

  return (
    <Styles.Root>
      <Styles.Container>
        <div>
          <img src={show.image.medium} alt="show poster" />
          <p>Show name: {show.name}</p>
          <p>Description: {removeTags(show.summary)}</p>
          <p>Language: {show.language}</p>
          <p>Genres: {show.genres.join(", ")}</p>
          <p>Score: {show.rating.average}/10</p>
          <p>Status: {show.status}</p>
        </div>

        <Suspense fallback={<Loading name={show.name} />}>
          <ShowEpisodes />
        </Suspense>
      </Styles.Container>
    </Styles.Root>
  );
};
export default ShowDetails;
Enter fullscreen mode Exit fullscreen mode

By making this modification, the ShowDetails component no longer requires to wait for the ShowEpisodes component to complete its loading process to be displayed.

The App.js file also now looks like this:

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

import ShowDetails from "./components/ShowDetails";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1 className="App-title">React Suspense Demo</h1>
      </header>

      <Suspense fallback={<p>loading...</p>}>
        <ShowDetails />
      </Suspense>
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

With the changes above, the following sequence will occur:

  • If the ShowDetails component has not finished loading, the "loading..." indicator will be displayed instead of the entire content area
  • Once the ShowDetails component has finished loading, the "loading..." indicator will be replaced by the actual content
  • Within the ShowDetails component itself, if the ShowEpisodes component has not finished loading, the <Loading name={show.name} /> component will be displayed in place of the ShowEpisodes component
  • Finally, once the ShowEpisodes component finishes loading, it will replace the <Loading name={show.name} /> component

React Suspense Demo Displaying Loaded Information One By One

Implementing React Suspense with React.lazy()

React.lazy()is another feature that pairs seamlessly with React Suspense. It allows you to load components lazily, which is especially beneficial for optimizing performance in large applications. In this section, we'll explore how to use React.lazy() in conjunction with Suspense to supercharge your app's performance.

The React.lazy() function enables the rendering of a dynamically imported component as a regular component. It simplifies the process of creating components that are loaded dynamically while being rendered like any other component. When the component is rendered, the bundle containing it is automatically loaded.

A component created using React.lazy() is loaded only when it is needed to be shown. During the loading process of the lazy component, it is advisable to display placeholder content, such as a loading indicator. This is where Suspense can be utilized.

When loading lazy components, you can display a loading indicator as placeholder content by providing a fallback prop to the suspense component. In essence, this allows you to specify a loading indicator that will be shown if the components within the tree below it are not yet prepared for rendering.

A basic example can be seen in the lazy-load branch here. In the App.js file, the Shows component is lazy-loaded by being imported with the React.lazy() function and then wrapped in a Suspense boundary:

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;
Enter fullscreen mode Exit fullscreen mode

Other async rendering methods: useState and useEffect

The useEffect and useState Hooks can be used together to perform asynchronous rendering in React apps. By combining useEffect's ability to perform side effects and useState's ability to keep track of a state value, you can mimic how Suspense asynchronously renders components.

Let’s look at an example to better understand this. As a reminder, asynchronous rendering is essentially the ability to render components and update the user interface in a nonblocking manner, and this is what we’ll be demonstrating with this example.

Just like previous examples, this example is available on GitHub under the use-effect-state branch. The src/components/Shows/index.js file has been edited now to look something like this:

import React, { useState, useEffect } from "react";
import * as Styles from "./styles";

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

const Shows = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const result = await fetch(
          "https://api.tvmaze.com/search/shows?q=heist"
        );
        const data = await result.json();
        setData(data);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    }
    fetchData();
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (....)
}
Enter fullscreen mode Exit fullscreen mode

In the code above, three local states help us to asynchronously render the Shows component. The data state contains the result of the external request to TV Maze’s API, the loading state indicates whether the data is currently being fetched (true) or not (false), and the error state holds any error that occurs during the data fetching process.

The useEffect Hook is used to fetch data from the TV shows API when the component mounts. It is an asynchronous function that uses fetch to make a GET request to the API endpoint. The fetched data is then stored in the data state using setData.

The loading state is set to false to indicate that the data fetching process is complete and if an error occurs during the fetch, it is stored in the error state using setError.

Now for rendering the actual component, the component renders different content based on the state:

  • If loading is true, it displays a loading message (<div>Loading...</div>)
  • If error is not null, it displays an error message with the error details
  • If neither loading nor error is true, it renders the TV show data

The above is quite similar to what was being done in earlier Suspense examples, albeit a bit more manually. We display the Loading… text when data is being fetched, and once it’s done, the actual TV shows data is rendered.

Conclusion

Async rendering is a crucial aspect of modern web development, and React Suspense in React 18 has emerged as a powerful tool for managing asynchronous operations seamlessly.

We've explored the fundamentals of React Suspense, and its practical implementation, and compared it with other async rendering methods. With the knowledge gained from this guide, you'll be well-equipped to make the most of async rendering and React Suspense in your next React project.


Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)