React Suspense for Data Fetching with Axios in React 18

React Suspense for Data Fetching with Axios in React 18

·

7 min read

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 the 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 is pending then it returns the promise itself. This causes the React Suspense to trigger the fallback component.

  • success- If the status of the promise if success then it returns the value returned after resolving the promise.

  • error - If the status of the promise is error 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..."

You can see the working example here

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:

you can see the working example here

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.

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.