Suspense
Using React 18 Suspense features with Apollo Client
"Suspense" is both a specific React API (<Suspense />
, a component which "lets you display a fallback until its children have finished loading"), and often used more generally to refer to a new way to build React apps using the concurrent rendering engine introduced in React 18.
In this guide, we'll look at Apollo Client's data fetching hooks introduced in 3.8 which leverage React's powerful Suspense features and walk through some examples of how they can be used.
To follow along with the examples below, open up our Suspense demo on CodeSandbox.
Fetching with Suspense
useSuspenseQuery
initiates a network request and causes the component calling it to suspend while the request is made. It can be thought of as a Suspense-ready replacement for useQuery
that allows you to take advantage of React's Suspense features while fetching during render.
Let's take a look at an example:
import { Suspense } from 'react';import {gql,TypedDocumentNode,useSuspenseQuery} from '@apollo/client';interface Data {dog: {id: string;name: string;};}interface Variables {id: string;}interface DogProps {id: string}const GET_DOG_QUERY: TypedDocumentNode<Data, Variables> = gql`query GetDog($id: String) {dog(id: $id) {# By default, an object's cache key is a combination of# its __typename and id fields, so we should always make# sure the id is in the response so our data can be# properly cached.idname}}`;function App() {return (<Suspense fallback={<div>Loading...</div>}><Dog id="3" /></Suspense>);}function Dog({ id }: DogProps) {const { data } = useSuspenseQuery(GET_DOG_QUERY, {variables: { id },});return <>Name: {data.dog.name}</>;}
Note: this example manually defines TypeScript interfaces for Data
and Variables
as well as the type for GET_DOG_QUERY
using TypedDocumentNode
. GraphQL Code Generator is a popular tool that will create these type definitions automatically for you. See the reference on Using TypeScript for more information on integrating GraphQL Code Generator with Apollo Client.
In this example, our App
component renders a Dog
component which fetches the record for a single dog via useSuspenseQuery
. When React attempts to render Dog
for the first time, the cache is unable to fulfill the request for the GetDog
query, so useSuspenseQuery
initiates a network request. Dog
suspends while the network request is pending, triggering the nearest Suspense
boundary above the suspended component in App
which renders our "Loading..." fallback. Once the network request is complete, Dog
renders with the newly cached name
for Mozzarella the Corgi.
You may have noticed that useSuspenseQuery
does not return a loading
boolean: this is because the component calling useSuspenseQuery
will always suspend when fetching data. A corollary is that when it does render, data
is always defined! Suspense fallbacks that exist outside of suspended components take the place of loading states that components were responsible for rendering themselves in the previous React paradigm.
Note for TypeScript users: since GET_DOG_QUERY
is a TypedDocumentNode
in which we have specified the result type via Data
generic type argument, the TypeScript type for data
returned by useSuspenseQuery
reflects that! This means that data
is guaranteed to be defined when Dog
renders, and that data.dog
will have the shape { id: string; name: string; breed: string; }
.
Changing variables
In the previous example, we fetched the record for a single dog by passing a hard-coded id
variable, Mozzarella, to useSuspenseQuery
. Now, let's say we want to fetch the record for a different dog using a dynamic value. We'll fetch the list of dogs with just their name
and id
, and once the user selects an individual dog we fetch more details, including their breed
.
Let's update our example:
export const GET_DOG_QUERY: TypedDocumentNode<DogData,Variables> = gql`query GetDog($id: String) {dog(id: $id) {idnamebreed}}`;export const GET_DOGS_QUERY: TypedDocumentNode<DogsData,Variables> = gql`query GetDogs {dogs {idname}}`;function App() {const { data } = useSuspenseQuery(GET_DOGS_QUERY);const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);return (<><selectonChange={(e) => setSelectedDog(e.target.value)}>{data.dogs.map(({ id, name }) => (<option key={id} value={id}>{dog.name}</option>))}</select><Suspense fallback={<div>Loading...</div>}><Dog id={selectedDog} /></Suspense></>);}function Dog({ id }: DogProps) {const { data } = useSuspenseQuery(GET_DOG_QUERY, {variables: { id },});return (<><div>Name: {data.dog.name}</div><div>Breed: {data.dog.breed}</div></>);}
Changing the dog via the select
will cause the component to suspend each time we select a dog whose record does not yet exist in the cache. Once we've loaded a given dog's record in the cache, however, selecting that dog again from our dropdown will not cause the component to re-suspend, since under our default cache-first
fetch policy Apollo Client will not make a network request after a cache hit.
Updating state without suspending
Sometimes we may want to avoid showing a loading UI in response to a pending network request and instead prefer to continue displaying the previous render. To do this, we can use a transition to mark our update as non-urgent. This tells React to keep the existing UI in place until the new data has finished loading.
To mark a state update as a transition, we use the startTransition
function from React.
Let's modify our example so that the previously displayed dog remains on the screen while the next one is fetched in a transition:
import { useState, Suspense, startTransition } from "react";function App() {const { data } = useSuspenseQuery(GET_DOGS_QUERY);const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);return (<><selectonChange={(e) => {startTransition(() => {setSelectedDog(e.target.value);});}}>{data.dogs.map(({ id, name }) => (<option key={id} value={id}>{name}</option>))}</select><Suspense fallback={<div>Loading...</div>}><Dog id={selectedDog} /></Suspense></>);}
By wrapping our setSelectedDog
state update in React's startTransition
function, we no longer see the Suspense fallback when selecting a new dog! Instead, the previous dog remains on the screen until the next dog's record has finished loading.
Showing pending UI during a transition
In the previous example, there is no visual indication that a fetch is happening when a new dog is selected. To provide nice visual feedback, let's update our example to use React's useTransition
hook which gives you an isPending
boolean value to determine when a transition is happening.
Let's dim the select dropdown while the transition is happening:
import { useState, Suspense, useTransition } from "react";function App() {const [isPending, startTransition] = useTransition();const { data } = useSuspenseQuery(GET_DOGS_QUERY);const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);return (<><selectstyle={{ opacity: isPending ? 0.5 : 1 }}onChange={(e) => {startTransition(() => {setSelectedDog(e.target.value);});}}>{data.dogs.map(({ id, name }) => (<option key={id} value={id}>{name}</option>))}</select><Suspense fallback={<div>Loading...</div>}><Dog id={selectedDog} /></Suspense></>);}
Rendering partial data
When the cache contains partial data, you may prefer to render that data immediately without suspending. To do this, use the returnPartialData
option.
Note: this option only works when combined with either the cache-first
(default) or cache-and-network
fetch policy. cache-only
is not currently supported by useSuspenseQuery
. For details on these fetch policies, see Setting a fetch policy.
Let's update our example to use the partial cache data and render immediately:
interface PartialData {dog: {id: string;name: string;};}const PARTIAL_GET_DOG_QUERY: TypedDocumentNode<PartialData,Variables> = gql`query GetDog($id: String) {dog(id: $id) {idname}}`;// Write partial data for Buck to the cache// so it is available when Dog rendersclient.writeQuery({query: GET_DOG_QUERY_PARTIAL,variables: { id: "1" },data: { dog: { id: "1", name: "Buck" } },});function App() {const client = useApolloClient();return (<Suspense fallback={<div>Loading...</div>}><Dog id="1" /></Suspense>);}function Dog({ id }: DogProps) {const { data } = useSuspenseQuery(GET_DOG_QUERY, {variables: { id },returnPartialData: true,});return (<><div>Name: {data?.dog?.name}</div><div>Breed: {data?.dog?.breed}</div></>);}
In this example, we write partial data to the cache for Buck to show the behavior when a query cannot be fully fulfilled from the cache. We tell useSuspenseQuery
that we are ok rendering partial data by setting the returnPartialData
option to true
. When Dog
renders for the first time, it does not suspend and uses the partial data immediately. Apollo Client will fetch the missing query data from the network in the background.
You will see Buck's name displayed immediately after the Name
label, followed by the Breed
label with no value. Once the missing fields have loaded, useSuspenseQuery
will update and Buck's breed will be displayed.
Note for TypeScript users: with returnPartialData
set to true
, the returned type for the data
property will mark all fields in the query type as optional. Apollo Client cannot accurately determine which fields are present in the cache at any given time when returning partial data.
Error handling
By default, both network errors and GraphQL errors are thrown by useSuspenseQuery
. These errors will be caught and displayed by the closest error boundary.
Note: An error boundary is a class component that implements static getDerivedStateFromError
. For more information, see the React docs for catching rendering errors with an error boundary.
Let's create a basic error boundary that renders an error UI when errors are thrown by our query:
class ErrorBoundary extends React.Component {constructor(props) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(error) {return { hasError: true };}render() {if (this.state.hasError) {return this.props.fallback;}return this.props.children;}}
Note: In a real application, your error boundary might need a more robust implementation. Consider using a library like react-error-boundary
when you need a high degree of flexibility and reusability.
When the GET_DOG_QUERY
inside of the Dog
component returns a GraphQL error or a network error, useSuspenseQuery
throws the error and the nearest error boundary renders its fallback component.
Our example doesn't have an error boundary yet—let's add one!
function App() {const { data } = useSuspenseQuery(GET_DOGS_QUERY);const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);return (<><selectonChange={(e) => setSelectedDog(e.target.value)}>{data.dogs.map(({ id, name }) => (<option key={id} value={id}>{name}</option>))}</select><ErrorBoundaryfallback={<div>Something went wrong</div>}><Suspense fallback={<div>Loading...</div>}><Dog id={selectedDog} /></Suspense></ErrorBoundary></>);}
Here, we're using our ErrorBoundary
component and placing it outside of our Dog
component. Now, when the useSuspenseQuery
hook inside the Dog
component throws an error, the ErrorBoundary
will catch it and display the fallback
element we've provided.
Note: when working with many React frameworks, you may see an error dialog overlay in development mode when errors are thrown, even with an error boundary. This is done to avoid the error being missed by developers.
Rendering partial data alongside errors
In some cases, you may want to render partial data alongside an error. To do this, set the errorPolicy
option to all
. useSuspenseQuery
will avoid throwing the error and instead set an error
property returned by the hook. To ignore errors altogether, set the errorPolicy
to ignore
. See the errorPolicy
documentation for more information.
Avoiding request waterfalls
Since useSuspenseQuery
suspends while data is being fetched, a tree of components that all use useSuspenseQuery
can cause a "waterfall", where each call to useSuspenseQuery
depends on the previous to complete before it can start fetching. This can be avoided by fetching with useBackgroundQuery
and reading the data with useReadQuery
.
useBackgroundQuery
initiates a request for data in a parent component and returns a queryRef
which is passed to useReadQuery
to read the data in a child component. When the child component renders before the data has finished loading, the child component will suspend.
Let's update our example to utilize useBackgroundQuery
:
import {useBackgroundQuery,useReadQuery,useSuspenseQuery,} from '@apollo/client';function App() {// We can start the request here, even though `Dog`// suspends and the data is read by a grandchild component.const [queryRef] = useBackgroundQuery(GET_BREEDS_QUERY);return (<Suspense fallback={<div>Loading...</div>}><Dog id="3" queryRef={queryRef} /></Suspense>);}function Dog({ id, queryRef }: DogProps) {const { data } = useSuspenseQuery(GET_DOG_QUERY, {variables: { id },});return (<>Name: {data.dog.name}<Suspense fallback={<div>Loading breeds...</div>}><Breeds queryRef={queryRef} /></Suspense></>);}interface BreedsProps {queryRef: QueryReference<BreedData>;}function Breeds({ queryRef }: BreedsProps) {const { data } = useReadQuery(queryRef);return data.breeds.map(({ characteristics }) =>characteristics.map((characteristic) => (<div key={characteristic}>{characteristic}</div>)));}
We begin fetching our GET_BREEDS_QUERY
when the App
component renders. The network request is made in the background while React renders the rest of our component tree. When the Dog
component renders, it fetches our GET_DOG_QUERY
and suspends.
When the network request for GET_DOG_QUERY
completes, the Dog
component unsuspends and continues rendering, reaching the Breeds
component. Since our GET_BREEDS_QUERY
request was initiated higher up in our component tree using useBackgroundQuery
, the network request for GET_BREEDS_QUERY
has already completed! When the Breeds
component reads the data from the queryRef
provided by useBackgroundQuery
, it avoids suspending and renders immediately with the fetched data.
Note: when the GET_BREEDS_QUERY
takes longer to fetch than our GET_DOG_QUERY
, useReadQuery
will suspend and the Suspense fallback inside of the Dog
component is rendered instead.
A note about performance
The useBackgroundQuery
hook used in a parent component is responsible for kicking off fetches, but doesn't deal with reading or rendering data. This is delegated to the useReadQuery
hook used in a child component. This separation of concerns provides a nice performance benefit because cache updates are observed by useReadQuery
and will re-render only the child component. You may find this as a useful tool to optimize your component structure to avoid unnecessarily re-rendering parent components when cache data changes.
Refetching and pagination
Apollo's Suspense data fetching hooks return functions for refetching query data via the refetch
function, and fetching additional pages of data via the fetchMore
function.
Let's update our example by adding the ability to refetch breeds. We destructure the refetch
function from the second item in the tuple returned from useBackgroundQuery
. We'll be sure to show a pending state to let the user know that data is being refetched by utilizing React's useTransition
hook:
import { Suspense, useTransition } from "react";import {useSuspenseQuery,useBackgroundQuery,useReadQuery,gql,TypedDocumentNode,QueryReference,} from "@apollo/client";function App() {const [isPending, startTransition] = useTransition();const [queryRef, { refetch }] = useBackgroundQuery(GET_BREEDS_QUERY);function handleRefetch() {startTransition(() => {refetch();});};return (<Suspense fallback={<div>Loading...</div>}><Dogid="3"queryRef={queryRef}isPending={isPending}onRefetch={handleRefetch}/></Suspense>);}function Dog({id,queryRef,isPending,onRefetch,}: DogProps) {const { data } = useSuspenseQuery(GET_DOG_QUERY, {variables: { id },});return (<>Name: {data.dog.name}<Suspense fallback={<div>Loading breeds...</div>}><Breeds isPending={isPending} queryRef={queryRef} /></Suspense><button onClick={onRefetch}>Refetch!</button></>);}function Breeds({ queryRef, isPending }: BreedsProps) {const { data } = useReadQuery(queryRef);return data.breeds.map(({ characteristics }) =>characteristics.map((characteristic) => (<divstyle={{ opacity: isPending ? 0.5 : 1 }}key={characteristic}>{characteristic}</div>)));}
In this example, every time the user clicks the "Refetch!" button rendered by the Dog
component, our onRefetch
handler calls the refetch
function returned by useBackgroundQuery
which initiates a new network request for the GET_BREEDS_QUERY
query. We wrap this in a transition by calling the startTransition
function provided by the useTransition
hook to avoid rendering our Suspense boundary. This maintains the existing UI while we fetch new data. To provide visual feedback that data is being fetched, we use the isPending
boolean to lower the opacity on the list of breeds.
Distinguishing between queries with queryKey
In rare cases, you might need to render multiple components that use the same query
and variables
combination. Due to the mechanics of Suspense, Apollo Client uses the combination of query
and variables
to uniquely identify the query when using Apollo's Suspense
data fetching hooks.
This presents a problem, however, because it means multiple hooks may end up with the same identity. These hooks will suspend together, regardless of which component initiates or re-initiates a network request.
You can prevent this with queryKey
option to ensure each hook has a unique identity. When queryKey
is provided, Apollo Client will use as part of the hook's identity in combination with its query
and variables
.
For more information, see the useSuspenseQuery
or useBackgroundQuery
API docs.
Skipping suspense hooks
While useSuspenseQuery
and useBackgroundQuery
both have a skip
option, that option is only present to ease migration from useQuery
with as few code changes as possible.
It should not be used in the long term.
Instead, you should use skipToken
:
import { skipToken, useSuspenseQuery } from '@apollo/client';const { data } = useSuspenseQuery(query,id ? { variables: { id } } : skipToken);
import { skipToken, useBackgroundQuery } from '@apollo/client';const [queryRef] = useBackgroundQuery(query,id ? { variables: { id } } : skipToken);
React Server Components (RSC)
Usage with Next.js 13 App Router
In Next.js v13, Next.js's new App Router brought the React community the first framework with full support for React Server Components (RSC) and Streaming SSR, integrating Suspense as a first-class concept from your application's routing layer all the way down.
In order to integrate with these features, our Apollo Client team released an experimental package, @apollo/experimental-nextjs-app-support
, which allows for seamless use of Apollo Client with both RSC and Streaming SSR, one of the first of its kind for data fetching libraries. See its README and our introductory blog post for more details.
Streaming while fetching with useBackgroundQuery
during streaming SSR
In a client-rendered application, useBackgroundQuery
can be used to avoid request waterfalls, but its impact can be even more noticeable in an application using streaming SSR as the App Router does. This is because the server can begin streaming content to the client, bringing even greater performance benefits.
Error handling
In a purely client-rendered app, errors thrown in components are always caught and displayed by the closest error boundary.
Errors thrown on the server when using one of the streaming server rendering APIs are treated differently. See the React documentation for more information.
Further reading
To view a larger codebase that makes use of Apollo Client's Suspense hooks (and many other new features introduced in Apollo Client 3.8), check out Apollo's Spotify Showcase on GitHub. It's a full-stack web application that pays homage to Spotify's iconic UI by building a clone using Apollo Client, Apollo Server and GraphOS.
useSuspenseQuery API
More details on useSuspenseQuery
's API can be found in its API docs.
useBackgroundQuery API
More details on useBackgroundQuery
's API can be found in its API docs.
useReadQuery API
More details on useReadQuery
's API can be found in its API docs.