Data fetching with Suspense in React
Suspense allows us to suspend component rendering. Suspense in React is not a library or server state manager; it’s literally what the word Suspense entails, a feature for managing asynchronous operations in a React app, keeping things in suspense until data are ready.
In this article, we will look at how Suspense works by fetching data from an API and rendering it in our application. In the course of this article, we will use Jsonplaceholder for our API.
Why Suspense?
Suspense aims to help with handling async operations by letting you wait for a code to load. It’s needed because users need their eyes fed with something like a spinner, so they know they expect data to be displayed. Concisely, Suspense will defer the execution of a component’s tree until the Promise is either resolved or rejected. Sites without proper handling of asynchronous operations are considered wretched sites.
How do we fetch data without Suspense? The following code is typical:
const [lyrics, isLoading] = fetchData("/lyrics");
if (isLoading) {
return <Spinner />;
}
return <Lyrics data={lyrics} />;
A variable isLoading
is used to track the status
of the request. If it’s true, we render a spinner
. The spinner is used for a better user interface, enabling the user to know that data is being fetched. There’s absolutely nothing wrong if we do it this way, but there are better ways of getting this done. Let’s see the simplest use case of Suspense:
const lyrics = fetchData("/lyrics");
return (
<Suspense fallback={<Spinner />}>
<Lyrics data={lyrics} />
</Suspense>
);
These are the changes that Suspense brought to the way we handle network calls:
- Instead of doing it by hand, we have Suspense rendering a fallback (spinner) declaratively.
- React didn’t know there was a network call, so we had to manage the loading state by ourselves. Using Suspense, React identifies that a network call is made and running.
- By wrapping the Lyrics component with Suspense, it suspends rendering data until the network call is done.
How does React know that a network call was made and is pending, is React that smart? Suspense renders a fallback component, but in no place in the code do we communicate to React that we are making a network request. This is where data fetching libraries such as Axios come into play. For the benefit of this tutorial, we will use Axios to communicate the loading state to React.
Data fetching using Suspense
Create a folder, head into your text editor, open your terminal and run the below commands;
npx create-react-app suspense
cd suspense
npm install react@rc react-dom@rc --save //we need to manually do it this way because Suspense is not yet stable.
npm install axios --save
npm start
To handle data fetching, we will need a folder; I named mine FetchApi
. This folder will have two files: Fetch.js
and WrapPromise.js
. These files will be responsible for fetching data from our API and communicating to Suspense. We will discuss more on this as we progress in the article for a better understanding.
Fetch.Js
We will use Axios to fetch Data. Once a request is made, Axios returns a promise that is fulfilled or declined; this depends on the backend server response. We will need the user’s profile from jsonplaceholder. We will have our asynchronous function this way:
import axios from "axios";
export async function fetchUser() {
return axios
.get("https://jsonplaceholder.typicode.com/users/3")
.then((res) => res.data)
.catch((err) => console.log(err));
}
We have exported our asynchronous function so that we can have access to it in our WrapPromise.js
component.
WrapPromise()
This is the most critical part of this tutorial because it communicates with Suspense. WrapPromise()
is more like an engine that wraps a promise and enables us to create a Resource
variable. This special Resource
variable will make rendering fetched data in our component a reality. I talked about Resource
in the next section but won’t go further with it here because it will make less sense. WrapPromise()
is likened to a wrapper, and this wrapper’s job is simply to wrap a promise and instigate a method that tells if the fetched data is completed or ready to be read.
WrapPromise()
takes in a Promise as an argument. The promise argument is either a network request that retrieves data from an API or a Promise object. When the promise is resolved, it returns the result, and if rejected, it throws an error. It uses the read()
method to read the current status of the promise.
Let me work you through how this function is used. I will paste everything in the wrapPromise.js
file, and below, I will break it down into understandable units afterward.
//WrapPromise.Js
const dataFetch = () => {
const userPromise = fetchUser;
return {
user: wrapPromise(userPromise),
};
};
const wrapPromise = (promise) => {
let status = "pending";
let result;
let suspend = promise().then(
(res) => {
status = "success";
result = res;
},
(err) => {
status = "error";
result = err;
}
);
return {
read() {
if (status === "pending") {
throw suspend;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
},
};
};
export default dataFetch;
Before the wrapper function was created, we had to create a dataFetch()
function that we could export to other components. In the dataFetch()
function, we created a variable userPromise
which is returned from fetchUser()
. What we aim to return from dataFetch()
is an object from User
. However, we are not returning the promise itself. Rather, we will return wrapPromise()
with the promise. It will look like this:
const dataFetch = () => {
const userPromise = fetchUser;
return {
user: wrapPromise(userPromise),
};
};
export default dataFetch;
To progress, we created the WrapPromise()
function. The WrapPromise()
function took in a promise as an argument. Below are the things done in the function.
- First, we need to set the initial
status
: by default, it is pending. - Secondly, we will want to store the
result
.
We create a variable called suspend
, which is to be assigned to the promise passed in. We will call the .then()
method. Inside this method, for the response:
- We want to set the
status
initialized above, which ispending
by default, tosuccess
. - Then, we set the result to whatever is passed in from
res
. If there is an error, we will set thestatus
equal toerror
and theresult
equal toerr
.
const wrapPromise = (promise) => {
let status = "pending";
let result;
let suspend = promise().then(
(res) => {
status = "success";
result = res;
},
(err) => {
status = "error";
result = err;
}
);
};
Now, regarding what we want to return in the wrapPromise()
is a method called read()
. In read()
, we simply want to check the status
:
- If the
status
is equal topending
, meaning it’s still fetching the data, we will throw thesuspend
, which will be caught with the.then()
. - If there is an
error
(that is,status
is equal to anerror
) we will throw theresults
. Remember that the result could have either thedata
or theerror
. In this case, it will have theerror
. - Finally, if the
status
is equal tosuccess
, which means everything went okay, we will simply return theresult
that will have thedata
.
{
if (status === "pending") {
throw suspend;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
}
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Display Fetched data
In this section, we have a component named profile.js
. This component communicates with the dataFetch()
function and stores the results in a special variable called Resource
, clarified in the steps below. The stored results are rendered in the component.
- We will import
dataFetch()
function intoprofile.js
component. - Then, declare a
resource
variable that calls thedataFetch()
function fromwrapPromise.js
file. Having aresource
variable is a common practice in the Suspense fetching method; it means the root of where the fetching was done. - Inside the components, we will use the
Resource
variable to get the user’s profile and declare theread()
function, telling React to read and render the data being fetched. - In the JSX we will render the user details and posts.
- In the main
App.js
component, we will wrap these components with Suspense, which carries afallback
spinner.
//profile.js
import React from "react";
import dataFetch from "./Api";
const resource = dataFetch();
const UserProfile = () => {
const user = resource.user.read();
return (
<div className="container">
<h1 className="title">{user.name}</h1>
<ul>
<li>username:{user.username}</li>
<li>phone:{user.phone}</li>
<li>website:{user.website}</li>
<li>email:{user.email}</li>
</ul>
</div>
);
};
export default UserProfile;
Using Suspense
In this section, we will import Suspense and UserProfile
into our App.js
component. We’ll go ahead to wrap our profile component with Suspense. We also have a spinner to improve the client’s user experience.
import React, { Suspense } from "react";
import "./App.css";
import UserProfile from "./profile";
import Spinner from "./spinner";
function App() {
return (
<div className="App">
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
</div>
);
}
export default App;
This is the result: when getting data, we get a spinner until the results come through.
Conclusion
In this article, we looked at Suspense, its data fetching steps, and samples. It is undoubtedly the perfect user experience and asynchronous operation handler tool in React.
React docs - suspense.