DEV Community

Cover image for How to use Suspense for data fetching in React
Smitter
Smitter

Posted on • Updated on

How to use Suspense for data fetching in React

React.Suspense lets you specify a loading indicator in case some components in the tree below it are not yet ready to render. It was introduced in React v16.6.

Summary: In this article we explore usecases of React.Suspense. We go over in-depth how we can use React.Suspense with data fetching and implement it artfully to make faster applications.

Let's see how we can use React.Suspense to write better React code and perfomant applications.

Use cases of React.Suspense.

1. Lazy-loading usecase.

You almost probably have used React.Suspense when lazy-loading components:

import React from "react";
import Spinner from "react-loading-indicators";

const AdminPortal = React.lazy(() => import("./AdminPortal")); // Lazy-loaded
const RegularPortal = React.lazy(() => import("./RegularPortal")); // Lazy-loaded

const Dash = (props) => {
  const { user } = props;

  return (
    <React.Suspense fallback={<Spinner />}>
      {user === "admin" ? <AdminPortal /> : <RegularPortal />}
    </React.Suspense>;
  )
};

export default Dash;
Enter fullscreen mode Exit fullscreen mode

From the snippet above, the lines:

const AdminPortal = React.lazy(() => import("./AdminPortal")); // Lazy-loaded
const RegularPortal = React.lazy(() => import("./RegularPortal")); // Lazy-loaded
Enter fullscreen mode Exit fullscreen mode

Means that we are lazy loading components. Using React.lazy enables us to render the lazy components like regular components.
The lazy component should be rendered inside a <React.Suspense> component, which allows us to show some fallback content (such as a loading indicator) while we are waiting for the lazy component to load.

When user of our application is a regular user(Not an "admin"), <RegularPortal /> component will be rendered and automatically the bundle containing RegularPortal is loaded. <AdminPortal /> component does not render and therefore the bundle containing AdminPortal IS NOT loaded.

On the other hand, the logic is the same. If the user of our application is an "admin", <AdminPortal /> component will be rendered and automatically the bundle containing AdminPortal is loaded. <RegularPortal /> component does not render and so the bundle containing RegularPortal IS NOT loaded.

The concept of lazy loading is applied since we are waiting to load certain bundles until they are needed.

We can "lazy-load" because of Code-splitting feature provided by build tools such as webpack, where some pieces of code are bundled into separate files/bundles (also known as chunk files) rather than including it all in one large main bundle.

This helps reduce the size of the main bundle and consequentially reduces initial page load time, initial page weight, and system resource usage when our application is served on the browser. Performance of our application becomes dramatically improved. We can then download the separate bundles(chunk files) only when the user action calls for that part of supporting bundle(chunk file).

Lazy loading components is the one use case supported by <React.Suspense>.

2. Data fetching usecase

Software developers are joined at the hip with API calls. Nevertheless, you need to know how to handle asynchronous requests in mighty frameworks like React - and in a good architectural point of view.

You can use <React.Suspense> for data fetching. In a good UX design, data fetching should be an asynchronous operation.

Before we can look at <React.Suspense> for data fetching, we will look at the traditional approach(Fetch-on-render), for instance data fetching inside useEffect

Traditional approach(Fetch-on-render).

In this approach:

Start rendering components. Each of these components may trigger data fetching in their effects and lifecycle methods.

import React, { useEffect, useState } from "react";
import Spinner from "react-loading-indicators";

import Post from "./Post";

function Posts() {
  const [isLoading, setIsLoading] = useState(true);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    setIsLoading(true);
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((response) => response.json())
      .then((json) => setPosts(json))
      .finally(() => setIsLoading(false));
  }, []);

  if (isLoading) {
    return <Spinner style={{ fontSize: "40px" }} color="black" />;
  }

  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "10px" }}>
      {posts.map((post, idx) => (
        <Post post={post} key={idx} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We call this approach fetch-on-render because it doesn't start fetching until after the component has rendered on the screen. Remember useEffect runs after a component has rendered.

This leads to a "waterfall" problem, whereby if we have yet another useEffect in <Post /> component(child) performing data fetching across network, it would happen after data fetching in useEffect of <Posts /> component(parent) has run to completion. So if fetching data(posts) in <Posts /> component takes three seconds, we’ll only fire request in <Post /> component after three seconds!

Suspense approach(Render-as-You-Fetch).

Suspense for Data Fetching is a new feature. It lets you also use <React.Suspense> to declaratively "wait" for anything else, including data. In this article, we focus on the data fetching use case, but it can also wait for images, scripts, or other asynchronous work.

With Suspense, we don’t wait to render before we can start fetching. In fact, we kick off the network request(fetching) and immediately start rendering:

import React from "react";
import Post from "./Post";

// Variables we will use to replay state of fetch() promise to React
let status = "pending";
let result;

// Start fetching posts even before rendering begins
const userId = JSON.parse(localStorage.getItem("authenticatedUser"))?.id;
const postsData = fetchPosts(userId);

// Posts component (definition)
const Posts = () => {
  // No need for loading states
  const posts = postsData();
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "10px" }}>
      {posts.map((post, idx) => (
        <Post post={post} key={idx} />
      ))}
    </div>
  );
};

// Fetch external data
function fetchPosts(userId) {
  let url = `https://jsonplaceholder.typicode.com/posts${
    userId ? "?userId=" + userId : ""
  }`;
  let fetching = fetch(url)
    .then((res) => res.json())
    // Fetch request has gone well
    .then((success) => {
      status = "fulfilled";

      result = success;
    })
    // Fetch request has failed
    .catch((error) => {
      status = "rejected";

      result = error;
    });

  return () => {
    if (status === "pending") {
      throw fetching; // Suspend(A way to tell React data is still fetching)
    } else if (status === "rejected") {
      throw result; // Result is an error
    } else if (status === "fulfilled") {
      return result; // Result is a fulfilled promise
    }
  };
}

export default Posts;
Enter fullscreen mode Exit fullscreen mode

In the above code, we are fetching posts from external source with a single line: const postsData = fetchPosts(userId);.

Inside <Posts /> component, we do not need to use any loading states nor fire a network request inside a useEffect - like we did with Traditional approach(Fetch-on-render). We get our posts by writing const posts = postsData();. We should get our posts if fetchPosts() has finished fetching. If data fetching is not yet finished, React will not render this component(<Posts />) just yet.

NOTE how we start fetching before rendering begins, that is, we start data fetching outside of the <Posts /> component definition. Fetching this way means that by the time react starts rendering <Posts /> component, we are already fetching for our data(posts). Hence a crucial aspect of Render-as-You-Fetch. Speed of application is improved drastically.

fetchPosts(id) function fetches external data.

We use Fetch API to fetch data from url.

We declared global variables: status and pending which we use to echo the state of the fetch() promise to React(whether it is pending, fulfilled or rejected):

const fetching = fetch(url)
  .then((res) => res.json())
  // Fetch request has gone well
  .then((success) => {
    status = "fulfilled";

    result = success;
  })
  // Fetch request has failed
  .catch((error) => {
    status = "rejected";

    result = error;
  });
Enter fullscreen mode Exit fullscreen mode

We store the fetch() promise in a fetching variable. And when the promise fulfills in the .then(), we shall replay its fulfilled state to react by setting status = "fulfilled"; and assign the response to result variable(result = success;).

Also when the promise rejects in the .catch() block, we also replay its rejected state to react by setting the associated variables.

In this piece of code:

if (status === "pending") {
  throw fetching; // Suspend (A way to tell React we are still waiting for results)
} else if (status === "rejected") {
  throw result; // Echo the failed Fetch() promise to React
} else if (status === "fulfilled") {
  return result; // Echo fulfilled promise to React
}
Enter fullscreen mode Exit fullscreen mode

We are simply just communicating the state of fetch() promise to react, so that React can determine if the component is ready to be rendered.

<Posts /> component can now be used with React.Suspense. We can render the <Posts /> component like:

import React from "react";
import Spinner from "react-loading-indicators";

import Posts from "./Posts";

// React Component to render
const MyPosts = () => {
  return (
    <div style={{ marginLeft: "20px" }}>
      <h1>My Posts</h1>
      {/* Wrap the <Posts /> component in React.Suspense to display a fallback(spinner)
      while the component is not yet ready to render (data is still fetching) */}

      <React.Suspense fallback={<Spinner style={{ fontSize: "40px" }} />}>
        <Posts />
      </React.Suspense>
    </div>
  );
};

export default MyPosts;
Enter fullscreen mode Exit fullscreen mode

Here’s what happens when we render <MyPosts /> on the screen:

  • We have fired the fetchPosts() data fetching when we imported Posts into MyPosts, that is import Posts from "./Posts";. This is so because in our Posts function snippet(refer above), fetchPosts() is defined and called outside of the <Posts /> component definition.

  • <MyPosts /> component will be run and rendered from top to bottom. When React renders <MyPosts /> component, it returns <Posts /> component as children.

  • React then runs <Posts /> component. <Posts /> component won't be rendered on the screen, if fetchPosts() is still fetching data. This component therefore "suspends". And React will skip over it to try and render other components in the tree.

  • There’s nothing left to try rendering. And because <Posts /> suspended, React displays the closest <React.Suspense> fallback above it in the tree: <Spinner style={{ fontSize: "40px" }} />. That's it for this render.

But we are not done. Since fetchPosts() data fetching request fired, more Data is streaming in. React will retry rendering. When data is finally fetched by the fetchPosts() function, the <Posts /> component will render successfully and we will no longer need the fallback component(<Spinner style={{ fontSize: "40px" }} />). Eventually, when data is successfully fetched, there will be no fallbacks on the screen.

Why use <React.Suspense> for data fetching

  • Avoid using loading states. We don't need to do checks - if (isLoading) show <Spinner />; otherwise render <Component /> - from our components. This removes boilerplate code.

  • Simplifies making quick design changes. For example we may create a new <DeletedPosts /> component and want to design <Posts /> and <DeletedPosts /> components to display on the web page at the same time even though they may be fetched from different endpoints hence finish fetching at different times. We could wrap them in a <React.Suspense> boundary like:

  <React.Suspense fallback={<Spinner variant="split-disc" />}>
    <Posts />
    <DeletedPosts />
  </React.Suspense>
Enter fullscreen mode Exit fullscreen mode

Or in a different design, we may want <Posts /> and <DeletedPosts /> components to have different loading states. We simply wrap them in different independent <React.Suspense> boundaries like:

  <React.Suspense fallback={<Spinner variant="split-disc" text="fetching posts..." />}>
    <Posts />
  </React.Suspense>
  <React.Suspense fallback={<Spinner style={{ fontSize: "40px" }} text="fetching deleted posts..." />}>
    <DeletedPosts />
  </React.Suspense>
Enter fullscreen mode Exit fullscreen mode
  • With Render-as-you-fetch aspect when implementing data fetching with <React.Suspense>, you get to create fast applications. We don’t want to delay data fetching across network until a component starts rendering.

  • The <React.Suspense> approach feels more like reading data synchronously in a component — as if it were already loaded. Hence improving readability of code.

Conclusion

In this article, we have looked at <React.Suspense> and how we can utilize it for lazy-loading components and data-fetching. Lazy-loading components useCase with <React.Suspense> is supported internally in React via <React.lazy>. We had to create custom patterns for data fetching useCase with <React.Suspense>.

Data fetching with <React.Suspense> has a major benefit of creating fast applications especially when you reiterate over Render-as-You-Fetch aspect.

Good to note:

As of version 18, React officially supports out of the box implementation of React.Suspense with lazy-loading components through React.lazy.

For this reason, using React.Suspense to let components express that they are "waiting" for data that is already being fetched, custom patterns need to be crafted. Using custom patterns may be a downside as implementation of React.Suspense may change in future.

In future, React plans to handle more scenarios using React.Suspense other than just "lazy-loading" - source:

In the future we plan to let Suspense handle more scenarios such as data fetching. You can read about this in our roadmap.

Today, lazy loading components is the only use case supported by <React.Suspense>

That's a wrap!

Thanks for reading this far. I believe you found the article useful.

You can find source code of what has been shared in this article here. Drop me a star🌟. Happy coding! 👨‍💻✨.


Follow me on twitter. You may want to catch up with other useful information I share.

Latest comments (2)

Collapse
 
jahnaviraj profile image
jahnaviraj

Tried the exact same thing,
Got this error - ReferenceError: localStorage is not defined

Collapse
 
smitterhane profile image
Smitter

Are you using nextjs? Nextjs renders a page while still at server side hence windo object is still undefined at this point. You may need to adjust your code to wait when window is available like:

useEffect(() => {
  // Perform localStorage action
  const item = localStorage.getItem('key')
}, [])
Enter fullscreen mode Exit fullscreen mode