Docs
Launch GraphOS Studio
Since 3.8.0

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.
id
name
}
}
`;
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 , 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 , 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) {
id
name
breed
}
}
`;
export const GET_DOGS_QUERY: TypedDocumentNode<
DogsData,
Variables
> = gql`
query GetDogs {
dogs {
id
name
}
}
`;
function App() {
const { data } = useSuspenseQuery(GET_DOGS_QUERY);
const [selectedDog, setSelectedDog] = useState(
data.dogs[0].id
);
return (
<>
<select
onChange={(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 (
<>
<select
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>
</>
);
}

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 (
<>
<select
style={{ 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) {
id
name
}
}
`;
// Write partial data for Buck to the cache
// so it is available when Dog renders
client.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 s 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 s in the query type as optional. Apollo Client cannot accurately determine which s 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 (
<>
<select
onChange={(e) => setSelectedDog(e.target.value)}
>
{data.dogs.map(({ id, name }) => (
<option key={id} value={id}>
{name}
</option>
))}
</select>
<ErrorBoundary
fallback={<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>}>
<Dog
id="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) => (
<div
style={{ 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.

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 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 .

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.

Previous
Queries
Next
Mutations
Edit on GitHubEditForumsDiscord