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.
@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 detectedDefer20220824Handler
- Implements the@defer
transport format that ships with Apollo RouterGraphQL17Alpha2Handler
- Implements the@defer
transport format that ships with GraphQL 17.0.0-alpha.2
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:
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.
@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:
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' id
s:
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.
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
loading | networkStatus | dataState | data | |
---|---|---|---|---|
query started | true | NetworkStatus.loading | "empty" | undefined |
initial response received | true | NetworkStatus.streaming | "streaming" | partial data , contains all non-deferred fields |
one or more deferred fragments received | true | NetworkStatus.streaming | "streaming" | partial data contains initial and some deferred fields |
final response received | false | NetworkStatus.ready | "complete" | data contains all fields, including deferred ones |
Refetching
loading | networkStatus | dataState | data | |
---|---|---|---|---|
query started | true | NetworkStatus.refetch | "complete" | previous data stays present |
initial response received | true | NetworkStatus.streaming | "streaming" | data contains previous data merged with incoming partial data |
one or more deferred fragments received | true | NetworkStatus.streaming | "streaming" | data gets more received data merged in |
final response received | false | NetworkStatus.ready | "complete" | data is updated with all newly received data |
Refetching with different variables
loading | networkStatus | dataState | data | |
---|---|---|---|---|
query started | true | NetworkStatus.refetch | "empty" | undefined |
initial response received | true | NetworkStatus.streaming | "streaming" | data contains initial fields |
one or more deferred fragments received | true | NetworkStatus.streaming | "streaming" | data gets more received data merged in |
final response received | false | NetworkStatus.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.