The React team regularly releases new features to help make our applications more efficient and manageable. One such feature is React Suspense.
By the end of this article, you'll understand how to use React Suspense to build smoother, faster, and more user-friendly React applications.
#What is React Suspense, and how does it work?
React Suspense is a built-in feature that simplifies managing asynchronous operations in your React applications.
Unlike data-fetching libraries like Axios or state management tools like Redux, Suspense focuses solely on managing what is displayed while your components wait for asynchronous tasks to complete.
#How React Suspense works
When React encounters a Suspense component, it checks if any child components are waiting for a promise to resolve. If so, React "suspends" the rendering of those components and displays a fallback UI, such as a loading spinner or message, until the promise is resolved.
Here's an example to illustrate how Suspense works:
<Suspense fallback={<div>Loading books...</div>}><Books /></Suspense>
In this code snippet, until the data for Books
is ready, the Suspense
component displays a fallback UI, in this case, a loading message. This clarifies to the user that the content is being fetched, providing a more seamless experience.
The fallback UI can be a paragraph, a component, or anything you prefer.
How it works with server-side rendering
React Suspense also enhances server-side rendering (SSR) by allowing you to render parts of your application progressively.
With SSR, you can use renderToPipeableStream
to load essential parts of your page first and progressively load the remaining parts as they become available. Suspense manages the fallbacks during this process, improving performance, user experience, and SEO.
#Data fetching patterns in React
When a React component needs data from an API, there are three common data fetching patterns: fetch on render, fetch then render, and render as you fetch (which is what React Suspense facilitates). Each pattern has its strengths and weaknesses.
Let's explore these patterns with examples to understand their nuances better.
Fetch on render
In this approach, the network request is triggered inside the component after it has mounted. This straightforward pattern can lead to performance issues, especially with nested components making similar requests.
const UserProfile = () => {const [user, setUser] = useState(null);useEffect(() => {fetch('/api/user').then(response => response.json()).then(data => setUser(data));}, []);if (!user) return <p>Loading user profile...</p>;return (<div><h1>{user.name}</h1><p>{user.bio}</p></div>);};
In this example, the fetch
request is triggered in the useEffect
hook after the component mounts. The loading state is managed by checking if the user
data is available.
This approach can lead to a network waterfall effect, where each subsequent component waits for the previous one to fetch data, causing delays.
Fetch then render
The fetch-then-render approach initiates the network request before the component mounts, ensuring data is available as soon as the component renders.
This pattern helps avoid the network waterfall problem seen in the on-render approach.
const fetchUserData = () => {return fetch('/api/user').then(response => response.json());};const App = () => {const [user, setUser] = useState(null);useEffect(() => {fetchUserData().then(data => setUser(data));}, []);if (!user) return <p>Loading user data...</p>;return (<div><UserProfile user={user} /></div>);};const UserProfile = ({ user }) => (<div><h1>{user.name}</h1><p>{user.bio}</p></div>);
In this example, the fetchUserData
function is called before the component mounts, and the data is set in the useEffect
hook. The loading state is managed similarly by checking if the user
data is available.
This method starts fetching early but still waits for all promises to be resolved before rendering useful data, which can lead to delays if one request is slow.
Render as you fetch
React Suspense introduces the render-as-you-fetch pattern, allowing components to render immediately after initiating a network request.
This improves user experience by rendering UI elements as soon as data is available, without waiting for all data to be fetched.
const fetchUserData = () => {let data;let promise = fetch('/api/user').then(response => response.json()).then(json => { data = json });return {read() {if (!data) {throw promise;}return data;}};};const resource = fetchUserData();const App = () => (<Suspense fallback={<p>Loading user profile...</p>}><UserProfile /></Suspense>);const UserProfile = () => {const user = resource.read();return (<div><h1>{user.name}</h1><p>{user.bio}</p></div>);};
In this example, the fetchUserData
function starts fetching data immediately and returns an object with a read
method. The read
method throws a promise if the data isn't ready, which triggers Suspense to show the fallback UI. Once available, read
returns the data, and the component renders it.
This pattern allows each component to manage its loading state independently, reducing wait times and improving the application's overall responsiveness.
#Use cases of Suspense
React Suspense can significantly enhance your applications' performance and user experience. Here are some practical use cases where Suspense shines:
1. Data fetching
One of the primary use cases of Suspense is managing data fetching in your applications. Using Suspense, you can display a loading state while fetching data from an API, providing a smoother user experience.
For example, the code below shows how the DataComponent
fetches data and displays a loading message until the data is available.
const fetchData = () => {let data;let promise = fetch('/api/data').then(response => response.json()).then(json => { data = json });return {read() {if (!data) {throw promise;}return data;}};};const resource = fetchData();const App = () => (<Suspense fallback={<p>Loading data...</p>}><DataComponent /></Suspense>);const DataComponent = () => {const data = resource.read();return (<div><h1>Data: {data.value}</h1></div>);};
The fetchData
function initiates a fetch
request and returns an object with a read
method. If the data isn't ready, the read
method throws a promise, which tells Suspense to display the fallback UI ("Loading data..."). Once available, read
returns the data, and DataComponent
renders it.
2. Lazy loading components
Suspense works seamlessly with React's lazy()
function to load components only when needed, reducing your application's initial load time. This is especially useful for large applications where not all components are required immediately.
For example, here is a LazyComponent
that is dynamically imported and lazy-loaded using React.lazy()
:
const LazyComponent = React.lazy(() => import('./LazyComponent'));const App = () => (<Suspense fallback={<p>Loading component...</p>}><LazyComponent /></Suspense>);
In this code, the <Suspense>
component specifies a fallback message ("Loading component...") to display while the LazyComponent
is being fetched and loaded.
3. Handling multiple asynchronous operations
Suspense can manage multiple asynchronous operations, ensuring that each part of the UI displays its loading state independently. This is useful in scenarios where different parts of the application fetch data from different sources.
const fetchUserData = () => {let data;let promise = fetch('/api/user').then(response => response.json()).then(json => { data = json });return {read() {if (!data) {throw promise;}return data;}};};const fetchPostsData = () => {let data;let promise = fetch('/api/posts').then(response => response.json()).then(json => { data = json });return {read() {if (!data) {throw promise;}return data;}};};const userResource = fetchUserData();const postsResource = fetchPostsData();const App = () => (<div><Suspense fallback={<p>Loading user...</p>}><UserProfile /></Suspense><Suspense fallback={<p>Loading posts...</p>}><Posts /></Suspense></div>);const UserProfile = () => {const user = userResource.read();return (<div><h1>{user.name}</h1><p>{user.bio}</p></div>);};const Posts = () => {const posts = postsResource.read();return (<ul>{posts.map(post => (<li key={post.id}>{post.title}</li>))}</ul>);};
In this code, there are two asynchronous operations: fetching user data and fetching posts data. Each fetch function returns an object with a read
method that throws the promise if the data isn't ready.
The App
component uses two <Suspense>
components, each with its own fallback UI ("Loading user..." and "Loading posts..."). This setup allows the UserProfile
and Posts
components to manage their loading states independently, improving the application’s overall responsiveness.
You can also nest <Suspense>
components to manage rendering order with Suspense:
const App = () => (<div><Suspense fallback={<p>Loading user profile...</p>}><UserProfile /><Suspense fallback={<p>Loading posts...</p>}><Posts /></Suspense></Suspense></div>);
This way, the outer <Suspense>
component wraps the UserProfile
component, displaying a fallback message ("Loading user profile...") while fetching user profile data, ensuring its details are shown first. Inside this outer <Suspense>
, another <Suspense>
wraps the Posts
component with its fallback message ("Loading posts..."), ensuring posts details render only after the user profile details are available.
This setup effectively manages loading states. It displays the outer fallback message until user profile data is fetched and then handles the post data loading state with the nested fallback message.
4. Server-side rendering (SSR)
Suspense can improve SSR by allowing you to specify which parts of the app should be rendered on the server and which should wait until the client has more data. This can significantly enhance the performance and SEO of your web application.
For example, the code below shows an App
component that uses <Suspense>
to specify a fallback UI ("Loading...") while the MainComponent
is being loaded.
import { renderToPipeableStream } from 'react-dom/server';const App = () => (<Suspense fallback={<p>Loading...</p>}><MainComponent /></Suspense>);// Server-side rendering logicconst { pipe } = renderToPipeableStream(<App />);
The renderToPipeableStream
function from react-dom/server
handles the server-side rendering, ensuring that the initial HTML sent to the client is rendered quickly and additional data is loaded progressively.
#How to use React Suspense
So far, we've gone over how React Suspense works and explored various scenarios with code examples. Now, let's apply what we've learned to a live project.
For this quick demo, we'll fetch blog posts from Hygraph (a headless CMS that leverages GraphQL to serve content to your applications) into a React application (styled with Tailwind CSS) and use Suspense to display skeleton post animations until the posts are loaded.
Step 1: Clone a Hygraph project
First, log in to your Hygraph dashboard and clone the Hygraph “Basic Blog" starter project. You can also choose any project from the marketplace that suits your needs.
Next, go to the Project settings page, navigate to Endpoints, and copy the High-Performance Content API endpoint. We'll use this endpoint to make API requests in our React project.
Step 2: Set up data fetching logic
When building a React application, especially one that deals with asynchronous data fetching, it's essential to manage the state of your data requests efficiently.
To do this, handle asynchronous and data-fetching operations in two files within the api
directory. This separation of concerns makes our code easier to read, test, and maintain. It also allows us to reuse the data-fetching logic across multiple components, promoting code reusability.
In your React project, create a folder named api
in the root directory. This folder will contain the files for handling data fetching and promise management.
Create two files in the api
folder: wrapPromise.js
and fetchData.js
.
wrapPromise.js
The wrapPromise.js
file contains a function designed to handle the state of a promise, which is an object representing the eventual completion or failure of an asynchronous operation.
export default function wrapPromise(promise) {let status = 'pending';let result;const suspender = promise.then((r) => {status = 'success';result = r;},(e) => {status = 'error';result = e;});return {read() {if (status === 'pending') {throw suspender;} else if (status === 'error') {throw result;} else if (status === 'success') {return result;}},};}
The function tracks a promise's state using status
('pending'
, 'success'
, or 'error'
) and result
(the resolved value or error). The suspender
handles the promise's resolution or rejection, updating the status and result accordingly.
The returned object includes a read
method that React's Suspense uses to check the promise's state: it throws the suspender
if pending, the error
if failed, or returns the result
if successful.
fetchData.js
The fetchData.js
file contains a function to perform the data fetching from the Hygraph GraphQL API.
import wrapPromise from './wrapPromise';function fetchData(url, query) {const promise = fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ query }),}).then((res) => {if (!res.ok) {throw new Error(`HTTP error! Status: ${res.status}`);}return res.json();}).then((data) => data.data).catch((error) => {console.error('Fetch error:', error);throw error;});return wrapPromise(promise);}export default fetchData;
The fetchData
function first imports wrapPromise
to manage the state of fetched data. It then constructs a POST request using the provided API endpoint (url
) and GraphQL query (query
).
Any errors during the fetch are caught, logged, and rethrown. Finally, the function returns the promise wrapped by wrapPromise
, enabling Suspense to handle its state.
Step 3: Create the React components
Now, let's create the React components that use Suspense to fetch and display data from Hygraph.
First, create a components
folder inside the src
folder. Within this components
folder, create a file named Posts.jsx
. This file handles making requests to the Hygraph API and displaying the fetched data.
Ensure you create a .env
file at the root of your project and add the Hygraph API endpoint.
import fetchData from '../../api/fetchData';const query = `{posts {idslugtitleexcerptcoverImage {url}publishedAtauthor {namepicture {url}}}}`;const resource = fetchData(import.meta.env.VITE_HYGRAPH_API, query);const Posts = () => {const { posts } = resource.read();return (<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">{posts.map((currentPost) => (<div className="relative h-[440px] mb-5" key={currentPost.id}><imgclassName="w-full h-52 object-cover rounded-lg"src={currentPost.coverImage.url}alt=""/><h2 className="text-xl font-semibold my-4">{currentPost.title}</h2><p className="text-gray-600 mb-2">{currentPost.excerpt}</p><div className="flex justify-between items-center absolute bottom-0 w-full"><div className="flex items-center"><imgclassName="w-10 h-10 rounded-full object-cover mr-2"src={currentPost.author?.picture.url}alt=""/><p className="text-sm text-gray-600">{currentPost.author?.name}</p></div><p className="text-sm text-gray-600">{new Date(currentPost.publishedAt).toLocaleDateString('en-us', {year: 'numeric',month: 'short',day: 'numeric',})}</p></div></div>))}</div>);};export default Posts;
In the code above, we import the fetchData
function from our previously created api
folder. This function handles the data fetching process. We then define a GraphQL query to fetch posts.
Using the fetchData
function, we pass the API endpoint (from the .env
file) and the query to fetch the data. The function returns a resource object that we can use to read the data when it's ready.
In the Posts
component, we call resource.read()
to obtain the data. This method is designed to integrate with React Suspense, allowing our component to wait for the data to be fetched. Once the data is available, we map over the posts and render them in a grid layout, displaying each post's title, excerpt, cover image, and author information.
Next, in the App.jsx
file, we import the Posts.jsx
file and implement React Suspense:
import { Suspense } from 'react';import Posts from './components/Posts';import PostSkeleton from './components/PostSkeleton';const App = () => {return (<div className="container mx-auto mt-24 px-5"><div className="text-5xl font-semibold my-10"><h1>Blog</h1></div><Suspense fallback={<PostSkeleton />}><Posts /></Suspense></div>);};export default App;
This setup ensures that users see a loading skeleton instead of a blank screen while the data is being fetched. Let’s create the skeleton.
In the components
folder, create a PostSkeleton.jsx
file and add the following code:
const PostSkeleton = () => {return (<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">{[...Array(5)].map((_, index) => (<div key={index} className="relative h-[400px] mb-5 animate-pulse"><div className="w-full h-52 bg-gray-200 rounded-lg"></div><div className="h-6 bg-gray-200 rounded mt-4 w-3/4"></div><div className="h-4 bg-gray-200 rounded mt-2 w-5/6"></div><div className="h-10 bg-gray-200 rounded mt-2 w-5/6"></div><div className="flex justify-between items-center absolute bottom-0 w-full mt-4"><div className="flex items-center"><div className="w-10 h-10 bg-gray-200 rounded-full mr-2"></div><div className="h-4 bg-gray-200 rounded w-20"></div></div><div className="h-4 bg-gray-200 rounded w-16"></div></div></div>))}</div>)}export default PostSkeleton
This component generates a grid of skeleton cards using the animate-pulse
class for a loading animation. This skeleton structure helps maintain a consistent layout and provides a better user experience during data fetching.
When you load the React application, you notice the Skeleton displays until the data is fetched from Hygraph.
#Handling errors
We need to implement an error boundary to ensure our application handles errors. This component catches any errors during data fetching or rendering, preventing the entire app from crashing and providing a user-friendly error message.
First, create an ErrorBoundary.jsx
file in the components
folder. This component acts as a wrapper around other components, catching errors in its child component tree.
import { Component } from 'react';class ErrorBoundary extends Component {constructor(props) {super(props);this.state = { hasError: false };}static defaultProps = {fallback: <h1>Something went wrong.</h1>,};static getDerivedStateFromError(error) {return { hasError: true };}componentDidCatch(error, errorInfo) {console.log(error, errorInfo);}render() {if (this.state.hasError) {return this.props.fallback;}return this.props.children;}}export default ErrorBoundary;
Next, we must modify App.jsx
to wrap the Suspense
component with the ErrorBoundary
. This setup ensures that the error boundary captures errors occurring during data fetching or rendering.
import { Suspense, lazy } from 'react';import Posts from './components/Posts';import PostSkeleton from './components/PostSkeleton';import ErrorBoundary from './components/ErrorBoundary';const App = () => {return (<div className="container mx-auto mt-24 px-5"><div className="text-5xl font-semibold my-10"><h1>Blog</h1></div><ErrorBoundary fallback={<div>Failed to fetch data!</div>}><Suspense fallback={<PostSkeleton />}><Posts /></Suspense></ErrorBoundary></div>);};export default App;
With this setup, our application can handle errors. For instance, if you tamper with the API URL in Posts.jsx
or any other issue during data fetching, the error boundary catches the error and displays the message "Failed to fetch data!" instead of crashing the entire app.
This ensures a better user experience even when something goes wrong in the background.
#Wrapping up
React Suspense is a powerful tool that simplifies handling asynchronous operations in React. By leveraging Suspense, you can improve your applications' performance and user experience.
Don't forget to sign up for a free-forever developer account on Hygraph to explore more about integrating React with a headless CMS.
Blog Author