Suspense for Data Fetching (Experimental)
.scary > blockquote { background-color: rgba(237, 51, 21, 0.2); border-left-color: #ed3315; }
Caution:
This page was about experimental features that aren't yet available in a stable release. It was aimed at early adopters and people who are curious.
Much of the information on this page is now outdated and exists only for archival purposes. Please refer to the React 18 Alpha announcement post for the up-to-date information.
Before React 18 is released, we will replace this page with stable documentation.
React 16.6 added a <Suspense>
component that lets you "wait" for some code to load and declaratively specify a loading state (like a spinner) while we're waiting:
Suspense for Data Fetching is a new feature that lets you also use <Suspense>
to declaratively "wait" for anything else, including data. This page focuses on the data fetching use case, but it can also wait for images, scripts, or other asynchronous work.
What Is Suspense, Exactly?
What Suspense Is Not
What Suspense Lets You Do
Using Suspense in Practice
What If I Don’t Use Relay?
For Library Authors
Traditional Approaches vs Suspense
Approach 1: Fetch-on-Render (not using Suspense)
Approach 2: Fetch-Then-Render (not using Suspense)
Approach 3: Render-as-You-Fetch (using Suspense)
Start Fetching Early
We’re Still Figuring This Out
Suspense and Race Conditions
Race Conditions with useEffect
Race Conditions with componentDidUpdate
The Problem
Solving Race Conditions with Suspense
Handling Errors
Next Steps
What Is Suspense, Exactly?
Suspense lets your components "wait" for something before they can render. In this example, two components wait for an asynchronous API call to fetch some data:
This demo is a teaser. Don't worry if it doesn't quite make sense yet. We'll talk more about how it works below. Keep in mind that Suspense is more of a mechanism, and particular APIs like fetchProfileData()
or resource.posts.read()
in the above example are not very important. If you're curious, you can find their definitions right in the demo sandbox.
Suspense is not a data fetching library. It's a mechanism for data fetching libraries to communicate to React that the data a component is reading is not ready yet. React can then wait for it to be ready and update the UI. At Facebook, we use Relay and its new Suspense integration. We expect that other libraries like Apollo can provide similar integrations.
In the long term, we intend Suspense to become the primary way to read asynchronous data from components -- no matter where that data is coming from.
What Suspense Is Not
Suspense is significantly different from existing approaches to these problems, so reading about it for the first time often leads to misconceptions. Let's clarify the most common ones:
It is not a data fetching implementation. It does not assume that you use GraphQL, REST, or any other particular data format, library, transport, or protocol.
It is not a ready-to-use client. You can't "replace"
fetch
or Relay with Suspense. But you can use a library that's integrated with Suspense (for example, new Relay APIs).It does not couple data fetching to the view layer. It helps orchestrate displaying the loading states in your UI, but it doesn't tie your network logic to React components.
What Suspense Lets You Do
So what's the point of Suspense? There are a few ways we can answer this:
It lets data fetching libraries deeply integrate with React. If a data fetching library implements Suspense support, using it from React components feels very natural.
It lets you orchestrate intentionally designed loading states. It doesn't say how the data is fetched, but it lets you closely control the visual loading sequence of your app.
It helps you avoid race conditions. Even with
await
, asynchronous code is often error-prone. Suspense feels more like reading data synchronously — as if it were already loaded.
Using Suspense in Practice
At Facebook, so far we have only used the Relay integration with Suspense in production. If you're looking for a practical guide to get started today, check out the Relay Guide! It demonstrates patterns that have already worked well for us in production.
The code demos on this page use a "fake" API implementation rather than Relay. This makes them easier to understand if you're not familiar with GraphQL, but they won't tell you the "right way" to build an app with Suspense. This page is more conceptual and is intended to help you see why Suspense works in a certain way, and which problems it solves.
What If I Don't Use Relay?
If you don't use Relay today, you might have to wait before you can really try Suspense in your app. So far, it's the only implementation that we tested in production and are confident in.
Over the next several months, many libraries will appear with different takes on Suspense APIs. If you prefer to learn when things are more stable, you might prefer to ignore this work for now, and come back when the Suspense ecosystem is more mature.
You can also write your own integration for a data fetching library, if you'd like.
For Library Authors
We expect to see a lot of experimentation in the community with other libraries. There is one important thing to note for data fetching library authors.
Although it's technically doable, Suspense is not currently intended as a way to start fetching data when a component renders. Rather, it lets components express that they're "waiting" for data that is already being fetched. Building Great User Experiences with Concurrent Mode and Suspense describes why this matters and how to implement this pattern in practice.
Unless you have a solution that helps prevent waterfalls, we suggest to prefer APIs that favor or enforce fetching before render. For a concrete example, you can look at how Relay Suspense API enforces preloading. Our messaging about this hasn't been very consistent in the past. Suspense for Data Fetching is still experimental, so you can expect our recommendations to change over time as we learn more from production usage and understand the problem space better.
Traditional Approaches vs Suspense
We could introduce Suspense without mentioning the popular data fetching approaches. However, this makes it more difficult to see which problems Suspense solves, why these problems are worth solving, and how Suspense is different from the existing solutions.
Instead, we'll look at Suspense as a logical next step in a sequence of approaches:
Fetch-on-render (for example,
fetch
inuseEffect
): Start rendering components. Each of these components may trigger data fetching in their effects and lifecycle methods. This approach often leads to "waterfalls".Fetch-then-render (for example, Relay without Suspense): Start fetching all the data for the next screen as early as possible. When the data is ready, render the new screen. We can't do anything until the data arrives.
Render-as-you-fetch (for example, Relay with Suspense): Start fetching all the required data for the next screen as early as possible, and start rendering the new screen immediately — before we get a network response. As data streams in, React retries rendering components that still need data until they're all ready.
Note
This is a bit simplified, and in practice solutions tend to use a mix of different approaches. Still, we will look at them in isolation to better contrast their tradeoffs.
To compare these approaches, we'll implement a profile page with each of them.
Approach 1: Fetch-on-Render (not using Suspense)
A common way to fetch data in React apps today is to use an effect:
We call this approach "fetch-on-render" because it doesn't start fetching until after the component has rendered on the screen. This leads to a problem known as a "waterfall".
Consider these <ProfilePage>
and <ProfileTimeline>
components:
If you run this code and watch the console logs, you'll notice the sequence is:
We start fetching user details
We wait...
We finish fetching user details
We start fetching posts
We wait...
We finish fetching posts
If fetching user details takes three seconds, we'll only start fetching the posts after three seconds! That's a "waterfall": an unintentional sequence that should have been parallelized.
Waterfalls are common in code that fetches data on render. They're possible to solve, but as the product grows, many people prefer to use a solution that guards against this problem.
Approach 2: Fetch-Then-Render (not using Suspense)
Libraries can prevent waterfalls by offering a more centralized way to do data fetching. For example, Relay solves this problem by moving the information about the data a component needs to statically analyzable fragments, which later get composed into a single query.
On this page, we don't assume knowledge of Relay, so we won't be using it for this example. Instead, we'll write something similar manually by combining our data fetching methods:
In this example, <ProfilePage>
waits for both requests but starts them in parallel:
The event sequence now becomes like this:
We start fetching user details
We start fetching posts
We wait...
We finish fetching user details
We finish fetching posts
We've solved the previous network "waterfall", but accidentally introduced a different one. We wait for all data to come back with Promise.all()
inside fetchProfileData
, so now we can't render profile details until the posts have been fetched too. We have to wait for both.
Of course, this is possible to fix in this particular example. We could remove the Promise.all()
call, and wait for both Promises separately. However, this approach gets progressively more difficult as the complexity of our data and component tree grows. It's hard to write reliable components when arbitrary parts of the data tree may be missing or stale. So fetching all data for the new screen and then rendering is often a more practical option.
Approach 3: Render-as-You-Fetch (using Suspense)
In the previous approach, we fetched data before we called setState
:
Start fetching
Finish fetching
Start rendering
With Suspense, we still start fetching first, but we flip the last two steps around:
Start fetching
Start rendering
Finish fetching
With Suspense, we don't wait for the response to come back before we start rendering. In fact, we start rendering pretty much immediately after kicking off the network request:
Here's what happens when we render <ProfilePage>
on the screen:
We've already kicked off the requests in
fetchProfileData()
. It gave us a special "resource" instead of a Promise. In a realistic example, it would be provided by our data library's Suspense integration, like Relay.React tries to render
<ProfilePage>
. It returns<ProfileDetails>
and<ProfileTimeline>
as children.React tries to render
<ProfileDetails>
. It callsresource.user.read()
. None of the data is fetched yet, so this component "suspends". React skips over it, and tries rendering other components in the tree.React tries to render
<ProfileTimeline>
. It callsresource.posts.read()
. Again, there's no data yet, so this component also "suspends". React skips over it too, and tries rendering other components in the tree.There's nothing left to try rendering. Because
<ProfileDetails>
suspended, React shows the closest<Suspense>
fallback above it in the tree:<h1>Loading profile...</h1>
. We're done for now.
This resource
object represents the data that isn't there yet, but might eventually get loaded. When we call read()
, we either get the data, or the component "suspends".
As more data streams in, React will retry rendering, and each time it might be able to progress "deeper". When resource.user
is fetched, the <ProfileDetails>
component will render successfully and we'll no longer need the <h1>Loading profile...</h1>
fallback. Eventually, we'll get all the data, and there will be no fallbacks on the screen.
This has an interesting implication. Even if we use a GraphQL client that collects all data requirements in a single request, streaming the response lets us show more content sooner. Because we render-as-we-fetch (as opposed to after fetching), if user
appears in the response earlier than posts
, we'll be able to "unlock" the outer <Suspense>
boundary before the response even finishes. We might have missed this earlier, but even the fetch-then-render solution contained a waterfall: between fetching and rendering. Suspense doesn't inherently suffer from this waterfall, and libraries like Relay take advantage of this.
Note how we eliminated the if (...)
"is loading" checks from our components. This doesn't only remove boilerplate code, but it also simplifies making quick design changes. For example, if we wanted profile details and posts to always "pop in" together, we could delete the <Suspense>
boundary between them. Or we could make them independent from each other by giving each its own <Suspense>
boundary. Suspense lets us change the granularity of our loading states and orchestrate their sequencing without invasive changes to our code.
Start Fetching Early
If you're working on a data fetching library, there's a crucial aspect of Render-as-You-Fetch you don't want to miss. We kick off fetching before rendering. Look at this code example closer:
Note that the read()
call in this example doesn't start fetching. It only tries to read the data that is already being fetched. This difference is crucial to creating fast applications with Suspense. We don't want to delay loading data until a component starts rendering. As a data fetching library author, you can enforce this by making it impossible to get a resource
object without also starting a fetch. Every demo on this page using our "fake API" enforces this.
You might object that fetching "at the top level" like in this example is impractical. What are we going to do if we navigate to another profile's page? We might want to fetch based on props. The answer to this is we want to start fetching in the event handlers instead. Here is a simplified example of navigating between user's pages:
With this approach, we can fetch code and data in parallel. When we navigate between pages, we don't need to wait for a page's code to load to start loading its data. We can start fetching both code and data at the same time (during the link click), delivering a much better user experience.
This poses a question of how do we know what to fetch before rendering the next screen. There are several ways to solve this (for example, by integrating data fetching closer with your routing solution). If you work on a data fetching library, Building Great User Experiences with Concurrent Mode and Suspense presents a deep dive on how to accomplish this and why it's important.
We're Still Figuring This Out
Suspense itself as a mechanism is flexible and doesn't have many constraints. Product code needs to be more constrained to ensure no waterfalls, but there are different ways to provide these guarantees. Some questions that we're currently exploring include:
Fetching early can be cumbersome to express. How do we make it easier to avoid waterfalls?
When we fetch data for a page, can the API encourage including data for instant transitions from it?
What is the lifetime of a response? Should caching be global or local? Who manages the cache?
Can Proxies help express lazy-loaded APIs without inserting
read()
calls everywhere?What would the equivalent of composing GraphQL queries look like for arbitrary Suspense data?
Relay has its own answers to some of these questions. There is certainly more than a single way to do it, and we're excited to see what new ideas the React community comes up with.
Suspense and Race Conditions
Race conditions are bugs that happen due to incorrect assumptions about the order in which our code may run. Fetching data in the useEffect
Hook or in class lifecycle methods like componentDidUpdate
often leads to them. Suspense can help here, too — let's see how.
To demonstrate the issue, we will add a top-level <App>
component that renders our <ProfilePage>
with a button that lets us switch between different profiles:
Let's compare how different data fetching strategies deal with this requirement.
Race Conditions with useEffect
useEffect
First, we'll try a version of our original "fetch in effect" example. We'll modify it to pass an id
parameter from the <ProfilePage>
props to fetchUser(id)
and fetchPosts(id)
:
Note how we also changed the effect dependencies from []
to [id]
— because we want the effect to re-run when the id
changes. Otherwise, we wouldn't refetch new data.
If we try this code, it might seem like it works at first. However, if we randomize the delay time in our "fake API" implementation and press the "Next" button fast enough, we'll see from the console logs that something is going very wrong. Requests from the previous profiles may sometimes "come back" after we've already switched the profile to another ID -- and in that case they can overwrite the new state with a stale response for a different ID.
This problem is possible to fix (you could use the effect cleanup function to either ignore or cancel stale requests), but it's unintuitive and difficult to debug.
Race Conditions with componentDidUpdate
componentDidUpdate
One might think that this is a problem specific to useEffect
or Hooks. Maybe if we port this code to classes or use convenient syntax like async
/ await
, it will solve the problem?
Let's try that:
This code is deceptively easy to read.
Unfortunately, neither using a class nor the async
/ await
syntax helped us solve this problem. This version suffers from exactly the same race conditions, for the same reasons.
The Problem
React components have their own "lifecycle". They may receive props or update state at any point in time. However, each asynchronous request also has its own "lifecycle". It starts when we kick it off, and finishes when we get a response. The difficulty we're experiencing is "synchronizing" several processes in time that affect each other. This is hard to think about.
Solving Race Conditions with Suspense
Let's rewrite this example again, but using Suspense only:
In the previous Suspense example, we only had one resource
, so we held it in a top-level variable. Now that we have multiple resources, we moved it to the <App>
's component state:
When we click "Next", the <App>
component kicks off a request for the next profile, and passes that object down to the <ProfilePage>
component:
Again, notice that we're not waiting for the response to set the state. It's the other way around: we set the state (and start rendering) immediately after kicking off a request. As soon as we have more data, React "fills in" the content inside <Suspense>
components.
This code is very readable, but unlike the examples earlier, the Suspense version doesn't suffer from race conditions. You might be wondering why. The answer is that in the Suspense version, we don't have to think about time as much in our code. Our original code with race conditions needed to set the state at the right moment later, or otherwise it would be wrong. But with Suspense, we set the state immediately -- so it's harder to mess it up.
Handling Errors
When we write code with Promises, we might use catch()
to handle errors. How does this work with Suspense, given that we don't wait for Promises to start rendering?
With Suspense, handling fetching errors works the same way as handling rendering errors -- you can render an error boundary anywhere to "catch" errors in components below.
First, we'll define an error boundary component to use across our project:
And then we can put it anywhere in the tree to catch errors:
It would catch both rendering errors and errors from Suspense data fetching. We can have as many error boundaries as we like but it's best to be intentional about their placement.
Next Steps
We've now covered the basics of Suspense for Data Fetching! Importantly, we now better understand why Suspense works this way, and how it fits into the data fetching space.
Suspense answers some questions, but it also poses new questions of its own:
If some component "suspends", does the app freeze? How to avoid this?
What if we want to show a spinner in a different place than "above" the component in a tree?
If we intentionally want to show an inconsistent UI for a small period of time, can we do that?
Instead of showing a spinner, can we add a visual effect like "greying out" the current screen?
Why does our last Suspense example log a warning when clicking the "Next" button?
To answer these questions, we will refer to the next section on Concurrent UI Patterns.
Last updated