Using the @defer directive in Apollo Client

Receive query response data incrementally


The @defer directive enables your queries to receive data for specific fields incrementally, instead of receiving all field data at the same time. This is helpful whenever some fields in a query take much longer to resolve than others.

Prerequisites

To use @defer with Apollo Client, you need to configure an incremental delivery format handler using the format you want to use.

note
The incremental delivery format used by @defer has not yet been standardized. There have been multiple proposals and varying implementations in the graphql package. Apollo Client does not default to a specific format for this reason.

Available handlers

Apollo Client provides the following incremental delivery handlers that are all exported from @apollo/client/incremental:

  • NotImplementedHandler (default) - Throws when @defer is detected

  • Defer20220824Handler - Implements the @defer transport format that ships with Apollo Router

  • GraphQL17Alpha2Handler - Implements the @defer transport format that ships with GraphQL 17.0.0-alpha.2

note
Defer20220824Handler and GraphQL17Alpha2Handler are aliases for the same delivery format and can be used interchangeably.

Configuration example

To enable @defer support, configure your Apollo Client instance with an incremental handler:

TypeScript
1import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
2import { Defer20220824Handler } from "@apollo/client/incremental";
3
4const client = new ApolloClient({
5  cache: new InMemoryCache(),
6  link: new HttpLink({ uri: "http://localhost:4000/graphql" }),
7  incrementalHandler: new Defer20220824Handler(),
8});

Without configuring an incremental handler, using the @defer directive results in an error.

note
For a query to defer fields successfully, the queried endpoint must also support the @defer directive. Entity-based @defer support is also at the General Availability stage in GraphOS Router and is compatible with all federation-compatible subgraph libraries.

Example

Let's say we're building a social media application that can quickly fetch a user's basic profile information, but retrieving that user's friends takes longer.

GraphQL allows us to declare all the fields our UI requires in a single query, but this also means that our query will be as slow as the field that takes the longest to resolve. The @defer directive allows us to mark parts of the query that are not necessary for our app's initial render which will be resolved once it becomes available.

To achieve this, we apply the @defer directive to an in-line fragment that contains all slow-resolving fields related to friend data:

GraphQL
1query PersonQuery($personId: ID!) {
2  person(id: $personId) {
3    # Basic fields (fast)
4    id
5    firstName
6    lastName
7
8    # Friend fields (slower)
9    ... @defer {
10      friends {
11        id
12      }
13    }
14  }
15}

Using this syntax, if the queried server supports @defer, our client can receive the "Basic fields" in an initial response payload, followed by a supplementary payload containing the "Friend fields".

Let's look at an example in React. Here's we can assume GET_PERSON is the above query, PersonQuery, with a deferred list of friends' ids:

JavaScript
app.jsx
1import { gql, useQuery } from "@apollo/client";
2
3function App() {
4  const { dataState, error, data } = useQuery(GET_PERSON, {
5    variables: {
6      id: 1,
7    },
8  });
9
10  if (error) return `Error! ${error.message}`;
11  if (dataState === "empty") return "Loading...";
12
13  return (
14    <>
15      Welcome, {data.firstName} {data.lastName}!
16      <details>
17        <summary>Friends list</summary>
18        {data.friends ?
19          <ul>
20            {data.friends.map((id) => (
21              <li>{id}</li>
22            ))}
23          </ul>
24        : null}
25      </details>
26    </>
27  );
28}

When our call to the useQuery hook first resolves with an initial payload of data, firstName and lastName will be populated with the values from the server and dataState will change from empty to streaming. Our deferred fields will not exist as keys on data yet, so we must add conditional logic that checks for their presence.

note
This example does not use the loading boolean to render the loading state. While the response is streaming, the loading flag remains true to indicate that the query has not yet fully completed. The networkStatus field starts as NetworkStatus.loading and changes to NetworkStatus.streaming as the initial chunk arrives. After all deferred data is received, loading becomes false and networkStatus is NetworkStatus.ready.

When subsequent chunks of deferred data arrive, useQuery re-renders and data includes the deferred data as it arrives.

For this reason, @defer can be thought of as a tool to improve initial rendering speeds when some slower data will be displayed below the fold or offscreen. In this case, we're rendering the friends list inside a <details> element which is closed by default, avoiding any layout shift as the friends data arrives.

loading, networkStatus, dataState, and data merging

These tables represent how loading, networkStatus, dataState, and data change as a query with the @defer directive is executed.

Starting a new query

loadingnetworkStatusdataStatedata
query startedtrueNetworkStatus.loading"empty"undefined
initial response receivedtrueNetworkStatus.streaming"streaming"partial data, contains all non-deferred fields
one or more deferred fragments receivedtrueNetworkStatus.streaming"streaming"partial data contains initial and some deferred fields
final response receivedfalseNetworkStatus.ready"complete"data contains all fields, including deferred ones

Refetching

loadingnetworkStatusdataStatedata
query startedtrueNetworkStatus.refetch"complete"previous data stays present
initial response receivedtrueNetworkStatus.streaming"streaming"data contains previous data merged with incoming partial data
one or more deferred fragments receivedtrueNetworkStatus.streaming"streaming"data gets more received data merged in
final response receivedfalseNetworkStatus.ready"complete"data is updated with all newly received data
note
As chunks arrive and replace existing data, data that cannot be merged, such as arrays without a type policy might replace existing "complete" data with "streaming" incomplete data.

Refetching with different variables

loadingnetworkStatusdataStatedata
query startedtrueNetworkStatus.refetch"empty"undefined
initial response receivedtrueNetworkStatus.streaming"streaming"data contains initial fields
one or more deferred fragments receivedtrueNetworkStatus.streaming"streaming"data gets more received data merged in
final response receivedfalseNetworkStatus.ready"complete"data contains all fields, including deferred ones

Using with code generation

If you currently use GraphQL Code Generator for your codegen needs, you'll need to use the Codegen version v4.0.0 or higher.

Usage in React Native

In order to use @defer in a React Native application, additional configuration is required. See the React Native docs for more information.

Feedback

Edit on GitHub

Ask Community