10 Minute Read: Understanding React Suspense with Visuals and Code
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:
- What is Suspense?
- 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
.
You should also check out:
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.
When <Child>
begins an asynchronous action, <Suspense>
”sees” it and switches to the loading UI.
When <Child>
throws an error, which could happen as a result of that asynchronous action, <ErrorBoundary>
”sees” it and switches to the error UI.
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 Promise
s 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:
- In the
onClick()
handler, thefetchCatFact()
function, which returns aPromise
, is wrapped with asuspensify()
function.
Thesuspensify()
function keeps track of thePromise
's state: pending, successful, or failed. It moves thePromise
's value behind aread()
method that, when called, communicates to the Suspense or Error Boundary using throw statements.
ThePromise
is saved in a React state variable to trigger a re-render. - The
catFact
constant reads thePromise
's value by calling itsread()
method. Remember, theread()
method was added bysuspensify()
.
Whenread()
is called, the method either throws aPromise
(when it’s loading), throws an error (when it’s errored), or returns the fetched cat fact (when it’s resolved).
When thePromise
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.

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! 🚀