Suspense
Use React 18 Suspense features with Apollo Client
"Suspense" is generally used to refer to a new way of building React apps using the concurrent rendering engine introduced in React 18. It's also a specific React API, <Suspense />
, a component that lets you display a fallback until its children have finished loading.
This guide explores Apollo Client's data fetching hooks introduced in 3.8, which leverage React's powerful Suspense features.
To follow along with the examples below, open up our Suspense demo on CodeSandbox.
Fetching with Suspense
The useSuspenseQuery
hook initiates a network request and causes the component calling it to suspend while the request is made. You can think of it as a Suspense-ready replacement for useQuery
that lets you take advantage of React's Suspense features while fetching during render.
Let's take a look at an example:
1import { Suspense } from 'react';
2import {
3 gql,
4 TypedDocumentNode,
5 useSuspenseQuery
6} from '@apollo/client';
7
8interface Data {
9 dog: {
10 id: string;
11 name: string;
12 };
13}
14
15interface Variables {
16 id: string;
17}
18
19interface DogProps {
20 id: string
21}
22
23const GET_DOG_QUERY: TypedDocumentNode<Data, Variables> = gql`
24 query GetDog($id: String) {
25 dog(id: $id) {
26 # By default, an object's cache key is a combination of
27 # its __typename and id fields, so we should always make
28 # sure the id is in the response so our data can be
29 # properly cached.
30 id
31 name
32 breed
33 }
34 }
35`;
36
37function App() {
38 return (
39 <Suspense fallback={<div>Loading...</div>}>
40 <Dog id="3" />
41 </Suspense>
42 );
43}
44
45function Dog({ id }: DogProps) {
46 const { data } = useSuspenseQuery(GET_DOG_QUERY, {
47 variables: { id },
48 });
49
50 return <>Name: {data.dog.name}</>;
51}
Data
and Variables
as well as the type for GET_DOG_QUERY
using TypedDocumentNode
. GraphQL Code Generator is a popular tool that creates 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. That's because the component calling useSuspenseQuery
always suspends when fetching data. A corollary is that when it does render, data
is always defined! In the Suspense paradigm, fallbacks that exist outside of suspended components replace the loading states components were previously responsible for rendering themselves.
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
has 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 to useSuspenseQuery
. Now, let's say we want to fetch the record for a different dog using a dynamic value. We'll fetch the name
and id
for our list of dogs, and once the user selects an individual dog, we fetch more details, including their breed
.
Let's update our example:
1export const GET_DOG_QUERY: TypedDocumentNode<
2 DogData,
3 Variables
4> = gql`
5 query GetDog($id: String) {
6 dog(id: $id) {
7 id
8 name
9 breed
10 }
11 }
12`;
13
14export const GET_DOGS_QUERY: TypedDocumentNode<
15 DogsData,
16 Variables
17> = gql`
18 query GetDogs {
19 dogs {
20 id
21 name
22 }
23 }
24`;
25
26function App() {
27 const { data } = useSuspenseQuery(GET_DOGS_QUERY);
28 const [selectedDog, setSelectedDog] = useState(
29 data.dogs[0].id
30 );
31
32 return (
33 <>
34 <select
35 onChange={(e) => setSelectedDog(e.target.value)}
36 >
37 {data.dogs.map(({ id, name }) => (
38 <option key={id} value={id}>{name}</option>
39 ))}
40 </select>
41 <Suspense fallback={<div>Loading...</div>}>
42 <Dog id={selectedDog} />
43 </Suspense>
44 </>
45 );
46}
47
48function Dog({ id }: DogProps) {
49 const { data } = useSuspenseQuery(GET_DOG_QUERY, {
50 variables: { id },
51 });
52
53 return (
54 <>
55 <div>Name: {data.dog.name}</div>
56 <div>Breed: {data.dog.breed}</div>
57 </>
58 );
59}
Changing the dog via the select
causes 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, selecting that dog again from our dropdown does not cause the component to re-suspend, since under our default cache-first
fetch policy Apollo Client does 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:
1import { useState, Suspense, startTransition } from "react";
2
3function App() {
4 const { data } = useSuspenseQuery(GET_DOGS_QUERY);
5 const [selectedDog, setSelectedDog] = useState(
6 data.dogs[0].id
7 );
8
9 return (
10 <>
11 <select
12 onChange={(e) => {
13 startTransition(() => {
14 setSelectedDog(e.target.value);
15 });
16 }}
17 >
18 {data.dogs.map(({ id, name }) => (
19 <option key={id} value={id}>{name}</option>
20 ))}
21 </select>
22 <Suspense fallback={<div>Loading...</div>}>
23 <Dog id={selectedDog} />
24 </Suspense>
25 </>
26 );
27}
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:
1import { useState, Suspense, useTransition } from "react";
2
3function App() {
4 const [isPending, startTransition] = useTransition();
5 const { data } = useSuspenseQuery(GET_DOGS_QUERY);
6 const [selectedDog, setSelectedDog] = useState(
7 data.dogs[0].id
8 );
9
10 return (
11 <>
12 <select
13 style={{ opacity: isPending ? 0.5 : 1 }}
14 onChange={(e) => {
15 startTransition(() => {
16 setSelectedDog(e.target.value);
17 });
18 }}
19 >
20 {data.dogs.map(({ id, name }) => (
21 <option key={id} value={id}>{name}</option>
22 ))}
23 </select>
24 <Suspense fallback={<div>Loading...</div>}>
25 <Dog id={selectedDog} />
26 </Suspense>
27 </>
28 );
29}
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.
cache-first
(default) or cache-and-network
fetch policy. cache-only
isn't 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:
1interface PartialData {
2 dog: {
3 id: string;
4 name: string;
5 };
6}
7
8const PARTIAL_GET_DOG_QUERY: TypedDocumentNode<
9 PartialData,
10 Variables
11> = gql`
12 query GetDog($id: String) {
13 dog(id: $id) {
14 id
15 name
16 }
17 }
18`;
19
20// Write partial data for Buck to the cache
21// so it is available when Dog renders
22client.writeQuery({
23 query: PARTIAL_GET_DOG_QUERY,
24 variables: { id: "1" },
25 data: { dog: { id: "1", name: "Buck" } },
26});
27
28function App() {
29 const client = useApolloClient();
30
31 return (
32 <Suspense fallback={<div>Loading...</div>}>
33 <Dog id="1" />
34 </Suspense>
35 );
36}
37
38function Dog({ id }: DogProps) {
39 const { data } = useSuspenseQuery(GET_DOG_QUERY, {
40 variables: { id },
41 returnPartialData: true,
42 });
43
44 return (
45 <>
46 <div>Name: {data?.dog?.name}</div>
47 <div>Breed: {data?.dog?.breed}</div>
48 </>
49 );
50}
In this example, we write partial data to the cache for Buck in order to show the behavior when a query cannot be entirely 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 then fetches the missing query data from the network in the background.
On first render, Buck's name is displayed after the Name
label, followed by the Breed
label with no value. Once the missing fields have loaded, useSuspenseQuery
triggers a re-render and Buck's breed is displayed.
returnPartialData
set to true
, the returned type for the data
property marks 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 are caught and displayed by the closest error boundary.
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:
1class ErrorBoundary extends React.Component {
2 constructor(props) {
3 super(props);
4 this.state = { hasError: false };
5 }
6
7 static getDerivedStateFromError(error) {
8 return { hasError: true };
9 }
10
11 render() {
12 if (this.state.hasError) {
13 return this.props.fallback;
14 }
15
16 return this.props.children;
17 }
18}
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!
1function App() {
2 const { data } = useSuspenseQuery(GET_DOGS_QUERY);
3 const [selectedDog, setSelectedDog] = useState(
4 data.dogs[0].id
5 );
6
7 return (
8 <>
9 <select
10 onChange={(e) => setSelectedDog(e.target.value)}
11 >
12 {data.dogs.map(({ id, name }) => (
13 <option key={id} value={id}>
14 {name}
15 </option>
16 ))}
17 </select>
18 <ErrorBoundary
19 fallback={<div>Something went wrong</div>}
20 >
21 <Suspense fallback={<div>Loading...</div>}>
22 <Dog id={selectedDog} />
23 </Suspense>
24 </ErrorBoundary>
25 </>
26 );
27}
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
catches it and displays the fallback
element we've provided.
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
. By setting this option, useSuspenseQuery
avoids throwing the error and instead sets 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 suspends.
Let's update our example to utilize useBackgroundQuery
:
1import {
2 useBackgroundQuery,
3 useReadQuery,
4 useSuspenseQuery,
5} from '@apollo/client';
6
7function App() {
8 // We can start the request here, even though `Dog`
9 // suspends and the data is read by a grandchild component.
10 const [queryRef] = useBackgroundQuery(GET_BREEDS_QUERY);
11
12 return (
13 <Suspense fallback={<div>Loading...</div>}>
14 <Dog id="3" queryRef={queryRef} />
15 </Suspense>
16 );
17}
18
19function Dog({ id, queryRef }: DogProps) {
20 const { data } = useSuspenseQuery(GET_DOG_QUERY, {
21 variables: { id },
22 });
23
24 return (
25 <>
26 Name: {data.dog.name}
27 <Suspense fallback={<div>Loading breeds...</div>}>
28 <Breeds queryRef={queryRef} />
29 </Suspense>
30 </>
31 );
32}
33
34interface BreedsProps {
35 queryRef: QueryRef<BreedData>;
36}
37
38function Breeds({ queryRef }: BreedsProps) {
39 const { data } = useReadQuery(queryRef);
40
41 return data.breeds.map(({ characteristics }) =>
42 characteristics.map((characteristic) => (
43 <div key={characteristic}>{characteristic}</div>
44 ))
45 );
46}
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.
GET_BREEDS_QUERY
takes longer to fetch than our GET_DOG_QUERY
, useReadQuery
suspends 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 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.
Fetching in response to user interactionSince 3.9.0
The useSuspenseQuery
and useBackgroundQuery
hooks let us load data as soon as the component calling the hook mounts. But what about loading a query in response to user interaction? For example, we may want to start loading some data when a user hovers on a link.
Available as of Apollo Client 3.9.0
, the useLoadableQuery
hook initiates a network request in response to user interaction.
useLoadableQuery
returns both an execution function and a queryRef
. The execution function initiates a network request when called with the provided variables. Like useBackgroundQuery
, passing the queryRef
to useReadQuery
in a child component suspends the child component until the query finishes loading.
queryRef
is null
until the execution function is called for the first time. For this reason, any child components attempting to read data using the queryRef
should be conditionally rendered.Let's update our example to start loading the dog's details as a result of selecting from a dropdown.
1import {
2 // ...
3 useLoadableQuery
4} from '@apollo/client';
5
6function App() {
7 const { data } = useSuspenseQuery(GET_DOGS_QUERY);
8 const [loadDog, queryRef] = useLoadableQuery(GET_DOG_QUERY);
9
10 return (
11 <>
12 <select
13 onChange={(e) => loadDog({ id: e.target.value })}
14 >
15 {data.dogs.map(({ id, name }) => (
16 <option key={id} value={id}>
17 {name}
18 </option>
19 ))}
20 </select>
21 <Suspense fallback={<div>Loading...</div>}>
22 {queryRef && <Dog queryRef={queryRef} />}
23 </Suspense>
24 </>
25 );
26}
27
28function Dog({ queryRef }: DogProps) {
29 const { data } = useReadQuery(queryRef)
30
31 return (
32 <>
33 <div>Name: {data.dog.name}</div>
34 <div>Breed: {data.dog.breed}</div>
35 </>
36 );
37}
We begin fetching our GET_DOG_QUERY
by calling the loadDog
function inside of the onChange
handler function when a dog is selected. Once the network request is initiated, the queryRef
is no longer null
, rendering the Dog
component.
useReadQuery
suspends the Dog
component while the network request finishes, then returns data
to the component. As a result of this change, we've also eliminated the need to track the selected dog id
in component state.
Initiating queries outside ReactSince 3.9.0
Starting with Apollo Client 3.9.0
, queries can be initiated outside of React. This allows your app to begin fetching data before React renders your components, and can provide performance benefits.
To preload queries, you first need to create a preload function with createQueryPreloader
. createQueryPreloader
takes an ApolloClient
instance as an argument and returns a function that, when called, initiates a network request.
ApolloClient
instance. This allows you to import that function directly without having to pass around your ApolloClient
instance each time you preload a query.The preload function returns a queryRef
that is passed to useReadQuery
to read the query data and suspend the component while loading. useReadQuery
ensures that your component is kept up-to-date with cache updates for the preloaded query.
Let's update our example to start loading the GET_DOGS_QUERY
before our app is rendered.
1import {
2 // ...
3 createQueryPreloader
4} from '@apollo/client';
5
6// This `preloadQuery` function does not have to be created each time you
7// need to preload a new query. You may prefer to export this function
8// alongside your client instead.
9const preloadQuery = createQueryPreloader(client);
10const preloadedQueryRef = preloadQuery(GET_DOGS_QUERY);
11
12function App() {
13 const { data } = useReadQuery(preloadedQueryRef);
14 const [loadDog, queryRef] = useLoadableQuery(GET_DOG_QUERY)
15
16 return (
17 <>
18 <select
19 onChange={(e) => loadDog({ id: e.target.value })}
20 >
21 {data.dogs.map(({ id, name }) => (
22 <option key={id} value={id}>{name}</option>
23 ))}
24 </select>
25 <Suspense fallback={<div>Loading...</div>}>
26 <Dog queryRef={queryRef} />
27 </Suspense>
28 </>
29 );
30}
31
32const root = createRoot(document.getElementById('app'));
33
34root.render(
35 <ApolloProvider client={client}>
36 <Suspense fallback={<div>Loading...</div>}>
37 <App />
38 </Suspense>
39 </ApolloProvider>
40);
We begin loading data as soon as our preloadQuery
function is called rather than waiting for React to render our <App />
component. Because the preloadedQueryRef
is passed to useReadQuery
in our App
component, we still get the benefit of suspense and cache updates.
queryRef
. When the component re-mounts, useReadQuery
automatically resubscribes to the queryRef
and any cache updates that occurred in the interim are read immediately as if the preloaded query had never been unmounted.Usage with data loading routers
Popular routers such as React Router and TanStack Router provide APIs to load data before route components are rendered. One example is the loader
function from React Router.
Loading data before rendering route components is especially useful for nested routes where data loading is parallelized. It prevents situations where parent route components might otherwise suspend and create request waterfalls for child route components.
preloadQuery
pairs nicely with these router APIs as it lets you take advantage of those optimizations without sacrificing the ability to rerender cache updates in your route components.
Let's update our example using React Router's loader
function to begin loading data when we transition to our route.
1import { useLoaderData } from 'react-router-dom';
2
3export function loader() {
4 return preloadQuery(GET_DOGS_QUERY);
5}
6
7export function RouteComponent() {
8 const queryRef = useLoaderData();
9 const { data } = useReadQuery(queryRef);
10
11 return (
12 // ...
13 );
14}
The
loader
function is available in React Router versions 6.4 and above.
React Router calls the loader
function which we use to begin loading the GET_DOG_QUERY
query by calling the preloadQuery
function. The queryRef
created by preloadQuery
is returned from the loader
function making it accessible in the route component.
When the route component renders, we access the queryRef
from the useLoaderData
hook, which is then passed to useReadQuery
. We get the benefit of loading our data early in the routing lifecycle, and the route component maintains the ability to rerender with cache updates.
preloadQuery
function only works with client-side routing. The queryRef
returned from preloadQuery
isn't serializable across the wire and as such, won't work with routers that fetch on the server such as Remix.Preventing route transitions until the query is loaded
By default, preloadQuery
works similarly to a deferred loader: the route transitions immediately and the incoming page that's attempting to read the data via useReadQuery
suspends until the network request finishes.
But what if we want to prevent the route from transitioning until the data is fully loaded? queryRef
's toPromise
method provides access to a promise that resolves when the network request has completed. This promise resolves with the queryRef
itself, making it easy to use with hooks such as useLoaderData
.
Here's an example:
1export async function loader() {
2 const queryRef = await preloadQuery(GET_DOGS_QUERY).toPromise();
3
4 return queryRef;
5}
6
7// You may also return the promise directly from loader.
8// This is equivalent to the above.
9export async function loader() {
10 return preloadQuery(GET_DOGS_QUERY).toPromise();
11}
12
13export function RouteComponent() {
14 const queryRef = useLoaderData();
15 const { data } = useReadQuery(queryRef);
16
17 // ...
18}
This instructs React Router to wait for the query to finish loading before the route transitions. When the route transitions after the promise resolves, the data is rendered immediately without the need to show a loading fallback in the route component.
Why prevent access to data
in toPromise
?
You may be wondering why we resolve toPromise
with the queryRef
itself, rather than the data loaded from the query. We want to encourage you to leverage useReadQuery
to avoid missing out on cache updates for your query. If data
were available, it would be tempting to consume it in your loader
functions and expose it to your route components. Doing so means missing out on cache updates.
If you need access to raw query data in your loader
functions, use client.query()
directly.
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:
1import { Suspense, useTransition } from "react";
2import {
3 useSuspenseQuery,
4 useBackgroundQuery,
5 useReadQuery,
6 gql,
7 TypedDocumentNode,
8 QueryRef,
9} from "@apollo/client";
10
11function App() {
12 const [isPending, startTransition] = useTransition();
13 const [queryRef, { refetch }] = useBackgroundQuery(
14 GET_BREEDS_QUERY
15 );
16
17 function handleRefetch() {
18 startTransition(() => {
19 refetch();
20 });
21 };
22
23 return (
24 <Suspense fallback={<div>Loading...</div>}>
25 <Dog
26 id="3"
27 queryRef={queryRef}
28 isPending={isPending}
29 onRefetch={handleRefetch}
30 />
31 </Suspense>
32 );
33}
34
35function Dog({
36 id,
37 queryRef,
38 isPending,
39 onRefetch,
40}: DogProps) {
41 const { data } = useSuspenseQuery(GET_DOG_QUERY, {
42 variables: { id },
43 });
44
45 return (
46 <>
47 Name: {data.dog.name}
48 <Suspense fallback={<div>Loading breeds...</div>}>
49 <Breeds isPending={isPending} queryRef={queryRef} />
50 </Suspense>
51 <button onClick={onRefetch}>Refetch!</button>
52 </>
53 );
54}
55
56function Breeds({ queryRef, isPending }: BreedsProps) {
57 const { data } = useReadQuery(queryRef);
58
59 return data.breeds.map(({ characteristics }) =>
60 characteristics.map((characteristic) => (
61 <div
62 style={{ opacity: isPending ? 0.5 : 1 }}
63 key={characteristic}
64 >
65 {characteristic}
66 </div>
67 ))
68 );
69}
In this example, our App
component renders a Dog
component that fetches a single dog's record via useSuspenseQuery
. When React attempts to render Dog
for the first time, the cache can't 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 the dog whose id="3"
: Mozzarella the Corgi.
Usage with query preloading
When loading queries outside React, the preloadQuery
function returns a queryRef
with no access to refetch
or fetchMore
functions. This presents a challenge when you need to refetch or paginate the preloaded query.
You can gain access to refetch
and fetchMore
functions by using the useQueryRefHandlers
hook. This hook integrates with React transitions, giving you the ability to refetch or paginate without showing the loading fallback.
Let's update our example to preload our GET_BREEDS_QUERY
outside React and use the useQueryRefHandlers
hook to refetch our query.
1// ...
2import {
3 // ...
4 useQueryRefHandlers,
5} from "@apollo/client";
6
7const queryRef = preloadQuery(GET_BREEDS_QUERY);
8
9function App() {
10 const [isPending, startTransition] = useTransition();
11 const { refetch } = useQueryRefHandlers(queryRef);
12
13 function handleRefetch() {
14 startTransition(() => {
15 refetch();
16 });
17 };
18
19 return (
20 <Suspense fallback={<div>Loading...</div>}>
21 <Dog
22 id="3"
23 isPending={isPending}
24 onRefetch={handleRefetch}
25 />
26 </Suspense>
27 );
28}
29
30// ...
We begin loading our GET_BREEDS_QUERY
outside of React with the preloadQuery
function. We pass the queryRef
returned from preloadQuery
to the useQueryRefHandlers
hook which provides us with a refetch
function we can use to refetch the query when the button is clicked.
Using useQueryRefHandlers
with query refs produced from other Suspense hooks
useQueryRefHandlers
can also be used in combination with any hook that returns a queryRef
, such as useBackgroundQuery
or useLoadableQuery
. This is useful when you need access to the refetch
and fetchMore
functions in components where the queryRef
is passed through deeply.
Let's update our example to use useBackgroundQuery
again and see how we can access a refetch
function in our Dog
component using useQueryRefHandlers
:
1function App() {
2 const [queryRef] = useBackgroundQuery(GET_BREEDS_QUERY);
3
4 return (
5 <Suspense fallback={<div>Loading...</div>}>
6 <Dog id="3" queryRef={queryRef} />
7 </Suspense>
8 );
9}
10
11function Dog({ id, queryRef }: DogProps) {
12 const { data } = useSuspenseQuery(GET_DOG_QUERY, {
13 variables: { id },
14 });
15 const [isPending, startTransition] = useTransition();
16 const { refetch } = useQueryRefHandlers(queryRef);
17
18 function handleRefetch() {
19 startTransition(() => {
20 refetch();
21 });
22 };
23
24 return (
25 <>
26 Name: {data.dog.name}
27 <Suspense fallback={<div>Loading breeds...</div>}>
28 <Breeds queryRef={queryRef} isPending={isPending} />
29 </Suspense>
30 <button onClick={handleRefetch}>Refetch!</button>
31 </>
32 );
33}
34
35// ...
useQueryRefHandlers
does not prevent you from using the handlers produced by query ref hooks. You can use the handlers in both locations, with or without React transitions to produce the desired result.Distinguishing between queries with queryKey
Apollo Client uses the combination of query
and variables
to uniquely identify each query when using Apollo's Suspense data fetching hooks.
If your application renders multiple components that use the same query
and variables
, this may present a problem: the queries made by multiple hooks share the same identity causing them to suspend at the same time, 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 uses it as part of the hook's identity in addition to 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
:
1import { skipToken, useSuspenseQuery } from '@apollo/client';
2const { data } = useSuspenseQuery(
3 query,
4 id ? { variables: { id } } : skipToken
5);
1import { skipToken, useBackgroundQuery } from '@apollo/client';
2const [queryRef] = useBackgroundQuery(
3 query,
4 id ? { variables: { id } } : skipToken
5);
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.
useLoadableQuery API
More details on useLoadableQuery
's API can be found in its API docs.
useQueryRefHandlers API
More details on useQueryRefHandlers
's API can be found in its API docs.
useReadQuery API
More details on useReadQuery
's API can be found in its API docs.