Suspense in React 18: How it works, and how you can use it
In this article, we explain the basics of what Suspense is, as well as make clear what is currently supported in React 18, as well as what’s not ready yet for production.
Oct 9, 2022 • 3 Minute Read
React 18, also known as the Concurrent React, released earlier this year and brought with it important changes. The most impactful one is the new concurrent rendering engine, which is what the new Concurrent Rendering feature “Suspense” is based on. If your React apps work with any asynchronous data sources (like REST services), using Suspense will not only make your apps easier to program, but those apps will also perform much better for your users.
In this article, we explain the basics of what Suspense is, as well as make clear what is currently supported in React 18, as well as what’s not ready yet for production.
What is Suspense, and Do I Need It?
Suspense is the first feature released by the Facebook React team that takes advantage of the new concurrent rendering engine built into React 18. It allows you to build apps with more responsive UIs that use less browser resources. It also gives developers and designers a more intuitive API to work with.
Suspense has been in the making for over three years, and it fundamentally changes how React determines what to render on a web page, based on your app’s component state changes. If your React app data (aka state data) changes - and these changes do not require a full page rerender from the server - the React rendering engine does the necessary updates to the UI.
An example of the difference between Concurrent React, rendering a page, and the React rendering engine prior is what happens when a web page updates a list based on some interruption (like typing in a text box that is used as a filter item on a list).
Without Concurrent Rendering, it's possible that many of the items in the list will get updated by React. This makes the page feel sluggish to the user, and the browser uses a lot of computer resources to make the app work at all.
If your browser allows you to view the animated gif below, this is what you might see.
If, on the same computer, you were using an app that implemented Concurrent Rendering — meaning that as the user typed into the search box, only what was necessary in the DOM was updated — you might see a UI that performs like the one below.
How Suspense Changes How You Implement Showing Data
Without Suspense
Let's assume you have some kind of external data source such as a remote web service, or even just data from a REST Server. Without using Suspense and Concurrent Rendering — which, by the way, is still an option in React 18 — you’d programmatically fetch the data, then check some data loading state, and finally, when that loading state indicates the data is fully retrieved, show the data in the UI.
Your app code probably looks something like this:
function CityList() {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetchCities().then((records) => {
setData(records);
setIsLoading(false);
});
}, []);
if (isLoading) return <div>Loading cities...</div>;
return (
<ul>
{data.map(({id, name}) => {
return <li key={id}>{name}</li>;
})}
</ul>
);
}
With Suspense
If you were to do the same thing with React 18 Suspense using Concurrent Rendering, you’d do things a bit differently.
Instead of using just a single component, you’d first create another component that wraps the rendering part of this component inside a new Suspense Element. Then, as an attribute to the Suspense element, you'd pass a fallback attribute, and that attribute would get assigned to a fallback UI that gets displayed when the data the component is not available to render (meaning it has not been completely returned from an external source).
Roughly, the code might now look something like this:
function CityListSuspenseWrapper() {
const specialPromiseResource = getSpecialPromiseTofetchCities();
return (
<Suspense fallback={<div>Loading cities...</div>}>
<CityList resource={specialPromiseResource} />
</Suspense>
)
}
function CityList({resource}) {
const data = resource.readData();
return (
<ul>
{data.map(({id, name}) => {
return <li key={id}>{name}</li>;
})}
</ul>
);
}
What's happening here, is the line of code const specialPromiseResource = getSpecialPromiseTofetchCities() is executed outside of our Suspense component, and it requests the data from the outside service or REST call. The call then returns immediately, and most importantly, returns with only a special resource reference to a promise about this particular data retrieval call.
Looking into our updated React CityList component, notice that we no longer have our programmatic calls that get executed after the page renders, that is the code passed into useEffect. Instead, we call resource.readData() and assume — for the sake of this component — that when data is returned, we can immediately render that data without being concerned about loading states.
What makes this work is the code inside our specialPromiseResource method is using special capabilities that cause our CityList component to render, when (and only when) the promise to get the city list data is complete. You can think about the call inside the CityList component — that is the line const data = resource.readData() — as helping React Concurrent Rendering figure out if it's time to render the city list to the UI.
The beauty of this code is that building components now to render data is much simpler. We don't need to worry about tracking loading state changes like we had to do before we had Suspense.
You might be thinking that this doesn't seem that different from the code we build prior to Suspense and Concurrent Rendering! That's probably true, but in this example, our particular React component only has to deal with one outside call to a single data source.
Think about the case of a complex web site, where a page may include data from many different external sources. The idea is that we set up all our special promise calls to get data, and then as the page renders and these external calls complete, different parts of the page render.
With our new Suspense implementation, the React Concurrent rendering engine can optimally figure out how to re-render the page based on what data arrives first, while at the same time, only rendering what is necessary to our browser DOM.
Imperative versus Declarative
From a user’s perspective, regardless of whether or not we use Concurrent Rendering, the result in the browser will basically be the same. The browser UI first displays a loading message, followed shortly by the UI updating to show the retrieved data.
The difference to us — as developers and designers — is significant. Without Suspense we have to programmatically track our loading and error states of all of our components. For a single external data call (like we have in our example above with our list of cities) the problem is not hard. However, when our component hierarchies get more complex, and we have multiple dependencies, tracking the states can get really confusing.
This type of code where we explicitly track and render based on things like state values we typically refer to as imperative programming — meaning we code exactly what we want, in the order we want it, with all the required conditionals along the way.
Declarative programming, on the other hand, is typically easier to write. It means instead of having to figure out what order everything should be done in, we state (or declare) our intention of what we want, and we let the application (or in our case, the new Concurrent Rendering engine) figure it out for us.
Coding with useEffect and isLoading as a declared state can be thought of as imperative programming, and coding with Suspense and fallback UIs can be thought of as declarative programming.
The Elephant in the Room
Ever hear the term “The Elephant in the Room?” It’s when there’s something important that needs to be discussed that everyone is aware of, but nobody wants to. Unfortunately, the latest version of React has one of those.
Here’s the elephant: In the case of React 18 Concurrent Rendering and Suspense, Suspense is fully released for production, but you can’t use it unless you program with a very opinionated and somewhat advanced programming API Relay for your data retrieval.
The reason the Facebook React team includes Suspense and Concurrent Rendering in React 18 is it is 100% production ready if you use Relay. As proof of that, it's currently what is driving Facebook.com, probably the highest volume and likely the most used site on the internet. If you want the benefits of Suspense and Concurrent Rendering now, you can build your apps with Relay.
If, however, you are like most of us developers and are using one of the other data access libraries (like SWR, React-Query, or even ApolloGraphQL) you will not be able to use Suspense at the moment.
The React team has made clear that their intention is to make Suspense work with APIs beyond Relay. Unfortunately for now, we just need to wait and make sure whatever API we use is production-ready for Suspense use.
Once the React team figures out a good way to integrate other API's into Suspense — and those teams do the implementations — everyone will be able to migrate to Suspense.
How to Learn to Program Suspense Now
The React team has published documentation that includes a very good example of how to use Suspense similar to what I did at the beginning of this post. Thinking about the pseudo code above where I call const specialPromiseResource = getSpecialPromiseTofetchCities() to get our special promise resource to be used in our Suspense wrapped component, as well as the call const data = resource.readData() in the data rendering component itself, it creates a working example with a "not for production" sample implementation of the necessary resources.
If you are thinking “Maybe I can use this code, and cut and paste it into my own apps and release them to production”, you should really think again. You'll find yourself copying these lines of commented-out code along with the rest!
// Suspense integrations like Relay implement
// a contract like this to integrate with React.
// Real implementations can be significantly more complex.
// Don't copy-paste this into your project!
Want to learn more about React 18?
I’ve released a course on Pluralsight called “What’s new in React 18”. It includes a significantly expanded example of using Suspense, as well as a more flexible implementation of the experimental library used for data and Suspense.
The example React app developed in the course shows how straightforward building async React apps like shown below are to build.
When you finish the course, you’ll have the skills and knowledge of the new features in React 18 so you can take advantage of these in all your existing and new React apps.