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:
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.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
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.
Get setup with LogRocket's modern React error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID.
- Install LogRocket via NPM or script tag.
LogRocket.init()
must be called client-side, not server-side. - (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>