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>
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
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;
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;
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;
}
},
};
};
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);
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;
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:
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;
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;
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 theShowEpisodes
component has not finished loading, the<Loading name={show.name} />
component will be displayed in place of theShowEpisodes
component - Finally, once the
ShowEpisodes
component finishes loading, it will replace the<Loading name={show.name} />
component
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;
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 (....)
}
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
istrue
, it displays a loading message (<div>Loading...</div>
) - If
error
is notnull
, it displays an error message with the error details - If neither
loading
norerror
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:
- 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.
NPM:
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
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>
3.(Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
Top comments (0)