NEW

Prismic offers an ideal solution to feature your e-commerce products in your promotional landing pages or inspirational content. View more

10 Minute Read: Understanding React Suspense with Visuals and Code

Written by Angelo Ashmore in House foundations icon. Foundations on July 19, 2022
React

React’s Suspense feature has been around for a while; it was released all the way back in October 2018 with React 16.6. We’ve seen multiple major versions of React introduce important changes to the library since then. Hooks, for example, and function components over class components have become the standard in React development in that time.

Despite being available for several years, however, most of us don’t use Suspense in our apps. Why is that? Should we be using it? Some of you might be muttering (or screaming), “I don’t even know what Suspense is!”

I’ll answer those questions, but first, let me address that last one.

In a sentence, React Suspense lets you handle loading states in your application in an idiomatic, React-y, sorta magical way.

“But hoooow?” you might respond. That’s going to take more than just a sentence to answer, but don’t worry, I won’t leave your hypothetical self hanging. To answer that question, and the previous ones as well, I’ll go over the following:

  1. What is Suspense?
  2. Where can it (or should it) be used?

Buckle up! We’re going on a suspense-ful tour of React Suspense.

What is React Suspense?

Practically speaking, <Suspense> is a first-party React component used to wrap other components that might make asynchronous requests. Any time a child component performs some action resulting in a loading state, such as a network request, a wrapping <Suspense> component can toggle its rendering to show a loading UI, like a <Spinner />.

 
<Suspense fallback={<Spinner />}>
  <DoesAsynchronousThings />
</Suspense>

In the above example, <DoesAsynchronousThings> might perform some kind of asynchronous action. When that action is triggered, <Suspense> will pick up on the fact that something below it is pending and display the <Spinner> component. Once the action is complete, <Suspense> will revert back to rendering <DoesAsynchronousThings>, now with its fetched content.

It’s worth noting that “some kind of asynchronous action” could be anything involving a Promise. It might be a network request. It might be a time-consuming mathematical computation. <Suspense> doesn’t care, as long as it’s contained within a Promise.

If you’re learning about Suspense, you should also learn about Error Boundaries

I’ve been referring to the Suspense component as “<Suspense>” so far, but it can go by another name as well: “Suspense Boundaries.” That name comes from a similar React concept: “Error Boundaries.”

If you’re not familiar with Error Boundaries, they are components used to wrap other components that may throw errors. Any time a child component throws an error, such as a failed network request, a wrapping Error Boundary component can toggle its rendering to show a custom error UI.

 
<ErrorBoundary fallback={<ErrorMessage />}>
  <MightThrowAnError />
</ErrorBoundary>

That should sound similar to <Suspense>: Suspense Boundaries react to loading child components, while Error Boundaries react to errored child components. In fact, they work using the same JavaScript instruction under the hood: the throw statement (as in throwing an error). JavaScript lets you throw anything, not just errors!

Error Boundaries and Suspense work together to create a stable user interface equipped to handle any state: loading, errored, or perfectly normal. You’ll see how the two concepts are intertwined in the next section when we visualize these concepts and turn them into code.

Unlike <Suspense>, React doesn’t provide a first-party <ErrorBoundary> component — you’ll need to make one yourself or use a library like react-error-boundary. If you want to learn more about Error Boundaries and how to create them, check out the official React Error Boundaries documentation.

Thinking about it visually

This is how I visualize Suspense (and Error Boundaries): <Child> begins an asynchronous action.

A diagram with an outer component, App, containing components nested within each other in the following order: Error Boundary, Suspense, Child. The Child component has a diamond inside it with the text, "Async Action" and is highlighted in blue to represent the start of that action.

When <Child> begins an asynchronous action, <Suspense> ”sees” it and switches to the loading UI.

The same diagram as above, except the Suspense component and the async action diamond are highlighted in blue. There's an arrow now from the async action to the Suspense component to represent the Suspense component recognizing and responding to the asynchronous action.

When <Child> throws an error, which could happen as a result of that asynchronous action, <ErrorBoundary> ”sees” it and switches to the error UI.

The same diagram as the two above, except now it's the Error Boundary component and the async action that are highlighted in blue. An arrow runs from the async action to the Error Boundary to represent the component recognizing an error and adjusting the UI accordingly.

Mesmerizing, isn’t it?

Under the hood, React implements these behaviors using the throw JavaScript statement, just like you would throw an Error. <Suspense> can “see” the loading state because the child component needs to throw the Promise (indeed, you can throw anything, not just Error objects). Likewise, <ErrorBoundary> can “see” the error because the child component throws the error. <Suspense> and <ErrorBoundary> ”catch” the thrown Promise or error (hence throw and catch, like throwing and catching a baseball).

<Suspense> and <ErrorBoundary> are essentially fancy try...catch statements.

And now for some code

Throwing errors isn’t unusual, but throwing Promises is, especially when you consider that it needs to be done in an encapsulated React component.

Luckily, there is a standard pattern you can follow to use Suspense.

To describe the pattern, let’s start from the very top of an example app and work our way down to an individual component that uses Suspense and Error Boundaries. The following <App> component contains the whole of our simple example application.

 
function App() {
  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback="Loading...">
        <DoesAsynchronousThings />
      </Suspense>
    </ErrorBoundary>
  )
}

The top-level <App> component renders three components:

  • <ErrorBoundary>: A component that will catch errors from anything below it and toggle its UI accordingly.
  • <Suspense>: A component that detect loading states from anything below it and toggle its UI accordingly.
  • <DoesAsynchronousThings>: A component that… does asynchronous things. For this example, let’s assume it makes a network request to an API that returns random cat facts when a button is clicked.

Next, let’s look at the <DoesAsynchronousThings> component to see how it’s implemented. We know it should make a network request when a button is clicked, but how does it communicate to the Suspense and Error Boundary components?

To see how the component interacts with the Suspense and Error Boundaries, read through the two files and their comments below. After you’ve read through the comments, I’ll explain how <DoesAsynchronousThings> is Suspense-compatible.

If you want to see the code in action, check out this StackBlitz project: ⚡️ React Suspense Demo on Stack Blitz.

 
// DoesAsynchronousThings.js

import { useState } from 'react'

// See the `lib/suspensify.js` code in the second tab.
import { suspensify } from './lib/suspensify'

// Renders a button that, when clicked, fetches and displays
// a random cat fact.
function DoesAsynchronousThings() {
  // Holds the pending Promise.
  const [promise, setPromise] = useState()

  // Triggers a network request, wraps the Promise in a
  // Suspense-aware interface, and sets the pending Promise.
  const onClick = () => {
    setPromise(suspensify(fetchCatFact()))
  }

  // If the pending Promise is set, try to read it.
  // The `read()` method is added by `suspensify()`
  const catFact = promise ? promise.read() : null

  return (
    <div>
      <button onClick={onClick}>Fetch Cat Fact</button>
      {catFact && <p>{catFact.fact}</p>}
    </div>
  )
}

// Fetches a random cat fact from https://catfact.ninja/.
async function fetchCatFact () {
  const res = await fetch('https://catfact.ninja/fact')

  return res.json()
}
 
// lib/suspensify.js

// A function that wraps a Promise with a Suspense-compatible
// interface.
//
// The Promise can be unwrapped using the return value's
// `read()` method.
export function suspensify(promise) {
  let status = "pending";

  // 1. Keep track of the Promise's state. The `status`
  //    variable will update as the Promise moves from
  //    pending to success or error.
  let result;
  let suspender = promise.then(
    (res) => {
      // On success, update the status to "success"
      status = "success";
      result = res;
    },
    (error) => {
      // On error, update the status to "error"
      status = "error";
      result = error;
    }
  );

  // 2. Return an object with a `read()` method that does one
  //    of the following:
  //
  //    a) Returns the Promise's resolved value if it's resolved.
  //    b) Sends a signal to a Suspense Boundary if the Promise is pending.
  //    c) Sends a signal to an Error Boundary if the Promise failed.
  return {
    read() {
      if (status === "pending") {
        // Pending promises are thrown and caught by <Suspense />.
        // FYI: Anything can be thrown in JavaScript, not just Errors.
        throw suspender;
      } else if (status === "error") {
        // Errors are thrown too, but are caught by an Error Boundary.
        throw result;
      } else if (status === "success") {
        // Finally, the Promise result is returned once it's resolved.
        return result;
      }
    },
  };
}

The <DoesAsynchronousThings> component is doing two unusual things which allow it to work with Suspense:

  1. In the onClick() handler, the fetchCatFact() function, which returns a Promise, is wrapped with a suspensify() function.

    The suspensify() function keeps track of the Promise's state: pending, successful, or failed. It moves the Promise's value behind a read() method that, when called, communicates to the Suspense or Error Boundary using throw statements.

    The Promise is saved in a React state variable to trigger a re-render.
  2. The catFact constant reads the Promise's value by calling its read() method. Remember, the read() method was added by suspensify().

    When read() is called, the method either throws a Promise (when it’s loading), throws an error (when it’s errored), or returns the fetched cat fact (when it’s resolved).

    When the Promise is thrown, <Suspense> will catch it, wait for it to resolve, then re-render the children. At that point, read() will be called again, but will return the resolved cat fact this time.

That’s it!

Okay, maybe it’s not the simplest React concept to understand after all, but hopefully Suspense’s inner workings are a little clearer now. You don’t necessarily need to understand what’s happening in the suspensify() function. Knowing where to use suspensify() and how to work with its value via read(), however, is important.

Where can it (or should it) be used?

Now that we all have a better understanding of Suspense, I can answer the questions posed at the start of the article.

Where can it be used?

Technically, Suspense can be used anywhere in your React tree if you’re using at least React 18. That includes within client-side rendering (i.e. CSR), server-side rendering (i.e. SSR), and experimental React Server Components (i.e. RSC, since we’re giving initialisms to everything).

If you’re using React 17 or earlier, you’re limited to using <Suspense> only with client-side rendering. That means you can’t use it in frameworks like Next.js unless you jump through some hoops to ensure the <Suspense> component, and everything below it, only render on the client.

Where should it be used?

At the time of writing, the React team only officially recommends using Suspense for lazy loading components with React.lazy(). However, the concept covers many other use cases, such as loading remote data.

Despite the React team’s recommendation, does that mean you — in 2022 — shouldn’t use it for other purposes, like data fetching? 🤔

Yes, in my opinion. Despite nearly four years of development since its first release, Suspense is still not ready for full-scale adoption across your application. Many implementation details needed for general Suspense use, like caching and server-side streaming support, are complex and not yet finalized. The React team is still actively expanding its behavior and exploring optimal uses.

Some of you may already be using Suspense in your apps for data fetching.

And that’s okay! Some data fetching libraries, like Relay, Apollo, and React Query, support Suspense today. The APIs they use are experimental and potentially come with limitations. You are welcome to use Suspense with these libraries, of course, but be aware that their behavior may change in the future when Suspense for data fetching is fully released.

So when can it actually be used, then?

Soon, probably! The React team is seemingly close to a solution where you absolutely should use it across your apps.

React Server Components, another long-awaited React core feature, is the driving force behind maturing Suspense and broadening its usefulness. The React team and others in the industry, including Vercel and Shopify, are steadily making progress on React Server Components (and Suspense as a result). We’ll first see general Suspense support with React Server Components in frameworks like Next.js and Hydrogen.

That means now is the time to familiarize yourself with Suspense. While APIs might change between now and its release, general concepts will likely stay the same.

If you want to get your hands dirty and try out Suspense with React Server Components, check out July 2022’s issue of The Optimized Dev, a newsletter that helps you stay on the leading edge of web development. You’ll be challenged to build an RSS feed reader in a modern Next.js app using the latest version of React Server Components and Suspense.

A cartoon graphics space scene showing an astronaut exploring space as rockets and planets float around.

Stay on the leading edge of development with a monthly coding challenge.

Explore new tech with a fun, scaffolded, coding challenge that lands in your inbox once a month. You'll get all of the learning with minimal spin-up.

Wrap-up

Suspense is simple in concept, but complex in its implementation. That’s why we don’t use it commonly in our apps today; it’s so complex that it hasn’t been fully realized yet.

Despite that, now is a good time to learn what Suspense is and understand where you can use it in the future. You’ll likely change the way you write React applications once it’s available. Stay ahead of the pack and prepare for that change.

Be sure to check out The Optimized Dev for a hands-on activity to learn more about Suspense and React Server Components.

To stay up to date on when Suspense is generally available for all use cases, check the React blog or follow the React Twitter feed.

I’d love to hear your thoughts on how you see Suspense changing your application development practices. If you have anything to share, feel free to reach out to me on Twitter: @angeloashmore.

Thanks for reading, and happy coding! 🚀

Profile picture

Angelo Ashmore

Senior Developer Experience Engineer at Prismic focusing on Gatsby, React, and TypeScript, in no particular order.

More posts