In this blog post, we will what React Suspense is and how it can be used for Data Fetching in React 18.
React Suspense has been in making for a long time, but it has now been released as a stable feature part of Concurrent React and makes use of the new Concurrent rendering engine released in React 18.
Suspense allows displaying a fallback component until the child component has finished loading the data.
Let's build an application that uses Suspense along with Axios to fetch data from the jsonplaceholder API to display a list of Posts.
Dead Simple Chat allows you to add chat in your React Applications using powerful JavaScript Chat API and SDK. With Dead Simple Chat you can add chat to your application in minutes.
Step 1: Scaffold your React Application
We will use create-react-app
to scaffold our application, open the terminal and cd
into the directory where you want to scaffold the application and run the following command:
npx create-react-app react-suspense-demo
cd react-suspense-demo
Step 2: Install Axios
Next, to use Axios in our application, we will install the Axios package using npm install.
npm install --save axios
Step 3: Create a custom hook for Suspense to work
We will first create a utility method to wrap the Axios request to work with React Suspense.
Create a file under the src/
folder called as useGetData.js
that will contain the code for our custom hook.
// src/useGetData.js
import { useState, useEffect } from "react";
import axios from "axios";
const promiseWrapper = (promise) => {
let status = "pending";
let result;
const s = promise.then(
(value) => {
status = "success";
result = value;
},
(error) => {
status = "error";
result = error;
}
);
return () => {
switch (status) {
case "pending":
throw s;
case "success":
return result;
case "error":
throw result;
default:
throw new Error("Unknown status");
}
};
};
function useGetData(url) {
const [resource, setResource] = useState(null);
useEffect(() => {
const getData = async () => {
const promise = axios.get(url).then((response) => response.data);
setResource(promiseWrapper(promise));
};
getData();
}, [url]);
return resource;
}
export default useGetData;
We created a utility method called the promiseWrapper
to wrap the Axios request, and then created a custom hook called as useGetData
that we will use in our components to send HTTP Get requests using Axios.
The hook uses a state variable called as the resource
to store resources we get after wrapping our Axios promise around our promiseWrapper
method.
Our promiseWrapper
method returns a function when called it runs the switch case on the current state of the promise and returns accordingly:
pending
- If the status of the promise ispending
then it returns the promise itself. This causes the React Suspense to trigger the fallback component.success
- If the status of the promise ifsuccess
then it returns the value returned after resolving the promise.error
- If the status of the promise iserror
then it throws an error
Step 4: Creating PostsComponet to display a list of posts
Now we will create a PostsComponet
that will use our custom hook, and call the jsonplaceholder.typicode.com/posts
API to fetch a list of mock posts.
// src/PostsComponent.js
import React from "react";
import useGetData from "./useGetData";
function PostsComponent() {
const data = useGetData("https://jsonplaceholder.typicode.com/posts");
return (
<div>
{data &&
data.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<hr />
<p>{post.body}</p>
</div>
))}
</div>
);
}
export default PostsComponent;
This component is very simple, we are calling our custom hook useGetData
and giving it an endpoint URL to fetch the list of posts.
Assing the result from the custom hook to a variable called as data
and displaying it on the screen.
Step 5: Wrapping our PostsComponent in Suspense
Finally, it's time to use our PostsComponent in our application open App.js
and add the following code to the file:
// src/App.js
import React, { Suspense } from "react";
import PostsComponent from "./PostsComponent";
function App() {
return (
<div className="App">
<Suspense fallback={<div>Loading Posts...</div>}>
<PostsComponent />
</Suspense>
</div>
);
}
export default App;
We are adding the <PostsComponent />
as a child to the <Suspense fallback={}></Suspense>
component.
The Suspense component has a fallback=
prop, here you can pass a component that you want to display until the child component loads.
In our example, we are showing just basic div which text "Loading Posts..."
Step 6: That's it
We have seen how to use Suspense
for data fetching in React 18, hopefully, the above example was helpful in understanding how to use the Suspense
component.
Adding Delay in Loading Post
To show clearly that our Suspense is working, we can add a setTimeout
in our promiseWrapper
const promiseWrapper = (promise, delay = 3000) => {
let status = "pending";
let result;
const s = promise
.then((value) => {
return new Promise((resolve) => {
setTimeout(() => {
status = "success";
result = value;
resolve(value);
}, delay);
});
})
.catch((error) => {
status = "error";
result = error;
});
return () => {
switch (status) {
case "pending":
throw s;
case "success":
return result;
case "error":
throw result;
default:
throw new Error("Unknown status");
}
};
};
And we will also update our App.js
file and added LoadingScreen
component to display a loading message.
// src/App.js
import React, { Suspense } from "react";
import PostsComponent from "./PostsComponent";
function LoadingScren() {
return (
<div>
<h1>Loading Posts</h1>
<h3>loading amazing posts for you to read</h3>
</div>
);
}
function App() {
return (
<div className="App">
<Suspense fallback={<LoadingScren />}>
<PostsComponent />
</Suspense>
</div>
);
}
export default App;
Here is the final result:
Comparison without using Suspense
Let's also see how our code would have looked if we had not used React Suspense.
We would have used a combination of useEffect
and useState
to achieve similar results.
Our code PostComponent.js
would look like this:
// src/PostsComponent.js
import React, { useEffect, useState } from "react";
import axios from "axios";
function PostsComponent() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
setIsLoading(false);
setData(response.data);
}
fetchPosts();
}, []);
if (isLoading) {
return <div>Loading Posts...</div>;
}
return (
<div>
{data &&
data.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<hr />
<p>{post.body}</p>
</div>
))}
</div>
);
}
export default PostsComponent;
We have used the useEffect
hook to make the API call to fetch the list of posts and set the result in a state variable that we will display.
To handle the loading state, we have created a state variable called as isLoading
and we are setting it to true
by default, once the posts are loaded we are updating the isLoading
state variable to false
.
Using JavaScript Chat SDK, integrate in-app chat in your React application in minutes. Dead Simple Chat is a highly scaleable chat solution that can be used for any chat use case.
Handing Errors in Suspense with Error Boundaries
React Suspense has support for ErrorBoundaries
, we can wrap the Suspense
component in an ErrorBoundary
and the error thrown by the Suspense's Child Component will be gracefully handled by the ErrorBoundary
component.
Let's update our example to use ErrorBoundary
.
Create a file called as src/ErrorBoundary.js
to hold our ErrorBoundary
component:
import { Component } from "react";
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Report the Error to Some Error Reporting Service.
console.error("Error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
The ErrorBoundary
can only be a class component, the other components in our application can be as-is functional components, but just the ErrorBoundary component has to be the class component because the lifecycle methods, componentDidCatch
and getDerviedStateFromError
are not available in functional components.
Our ErrorBoundary
component accepts a fallback
prop which is returned when an error is detected.
We can pass a component to the fallback
prop and that component will be displayed in case of an error.
Finally, we will update our App.js
code to use our ErrorBoundary
component:
// App.js
import React, { Suspense } from "react";
import ErrorBoundary from "./ErrorBoundary";
import PostsComponent from "./PostsComponent";
function LoadingScren() {
return (
<div>
<h1>Loading Posts</h1>
<h3>loading amazing posts for you to read</h3>
</div>
);
}
function App() {
return (
<div className="App">
<ErrorBoundary fallback={<div>Error Occurred when loading Post..</div>}>
<Suspense fallback={<LoadingScren />}>
<PostsComponent />
</Suspense>
</ErrorBoundary>
</div>
);
}
export default App;
That's it! Now when a network error occurs when loading the posts, our ErrorBoundary will handle it and display our fallback component which is a div tag with a message.
You might be interested in some of our other articles
- Data Fetching with React Suspense
- React useMemo Vs useCallback
- How to use useSyncExternalStore in React 18
- React useState: The Complete guide
Conclusion
Hopefully, this post was helpful in understanding the concept of React Suspense. We have looked at an example of how to use React Suspense with Axios to fetch data.
We also looked at the example of how we would have done data fetching without using Suspense and looked at an example of error handling using Suspense.