Back

Data fetching with Suspense in React

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 is pending by default, to success.
  • Then, we set the result to whatever is passed in from res. If there is an error, we will set the status equal to error and the result equal to err.
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 to pending, meaning it’s still fetching the data, we will throw the suspend, which will be caught with the .then().
  • If there is an error (that is, status is equal to an error) we will throw the results. Remember that the result could have either the data or the error. In this case, it will have the error.
  • Finally, if the status is equal to success, which means everything went okay, we will simply return the result that will have the data.
{
  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.

replayer.png

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 into profile.js component.
  • Then, declare a resource variable that calls the dataFetch() function from wrapPromise.js file. Having a resource 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 the read() 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 a fallback 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.