Since 3.8.0

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:

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

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

TypeScript
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:

TypeScript
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:

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

 note
This option only works when combined with either the 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:

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

 note
For TypeScript users: With 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.

 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:

TypeScript
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}
 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!

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

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

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

 note
When the 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.

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

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

💡 tip
Consider exporting your preload function along with your 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.

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

 note
Unmounting components that contain preloaded queries is safe and disposes of the 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.

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

 note
The 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:

TypeScript
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:

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

TypeScript
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:

TypeScript
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// ...
 note
Using the handlers returned from 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:

JavaScript
Recommended usage of skipToken with useSuspenseQuery
1import { skipToken, useSuspenseQuery } from '@apollo/client';
2const { data } = useSuspenseQuery(
3  query,
4  id ? { variables: { id } } : skipToken
5);
JavaScript
Recommended usage of skipToken with useBackgroundQuery
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.

Feedback

Edit on GitHub

Forums