Key arguments in Apollo Client

Using the keyArgs API


We recommend reading Core pagination API before learning about considerations specific to keyArgs configuration.

The Apollo Client cache can store multiple entries for a single schema field. By default, each entry corresponds to a different set of values for the field's arguments.

For example, consider this Query.user field:

GraphQL
1type Query {
2  # Returns whichever User object corresponds to `id`
3  user(id: ID!): User
4}

If we query for Users with ids 1 and 2, the Apollo Client cache stores entries for both like so:

JavaScript
Cache
1{
2  'ROOT_QUERY': {
3    'user({"id":"1"})': {
4      '__ref': 'User:1'
5    },
6    'user({"id":"2"})': {
7      '__ref': 'User:2'
8    }
9  }
10}

As shown above, each entry's storage key includes the corresponding argument values. This means that if any of a field's arguments differ between queries, the storage keys also differ, and those queries result in distinct cache entries.

If a field has no arguments, its storage key is just its name.

This default behavior is for safety: the cache doesn't know whether it can merge the values returned for different argument combinations without invalidating data. In the example above, the cache definitely shouldn't merge the results of querying for Users with ids 1 and 2.

Pagination issues

Certain arguments shouldn't cause the Apollo Client cache to store a separate entry. This is almost always the case for arguments related to paginated lists.

Consider this Query.feed field:

GraphQL
1type Query {
2  feed(offset: Int, limit: Int, category: Category): [FeedItem!]
3}

The offset and limit arguments enable a client to specify which "page" of the feed it wants to fetch. In an app with an infinitely scrolling feed, the client might initially fetch the first ten items, then fetch the next ten:

GraphQL
1# First query
2query GetFeedItems {
3  feed(offset: 0, limit: 10, category: "SPORTS")
4}
5
6# Second query
7query GetFeedItems {
8  feed(offset: 10, limit: 10, category: "SPORTS")
9}

But because their argument values differ, these two lists of ten items are cached separately by default. This means that when the second query completes, the returned items aren't appended to the original list in the feed!

JavaScript
Cache
1{
2  'ROOT_QUERY': {
3    // First query
4    'feed({"offset":"0","limit":"10","category":"SPORTS"})': [
5      {
6        '__ref': 'FeedItem:1'
7      },
8      // ...additional items...
9    ],
10    // Second query
11    'feed({"offset":"10","limit":"10","category":"SPORTS"})': [
12      {
13        '__ref': 'FeedItem:11'
14      },
15      // ...additional items...
16    ]
17  }
18}

In this case, we don't want offset or limit to be included in a cache entry's storage key. Instead, we want the cache to merge the results of the two above queries into a single cache entry that includes the items from both lists.

To help handle this case, we can set key arguments for the field.

Setting keyArgs

A key argument is an argument for a GraphQL field that's included in cache storage keys for that field. By default, all GraphQL arguments are key arguments, as shown in our feed example:

JavaScript
Cache
1{
2  'ROOT_QUERY': {
3    // First query
4    'feed({"offset":"0","limit":"10","category":"SPORTS"})': [
5      {
6        '__ref': 'FeedItem:1'
7      },
8      // ...additional items...
9    ],
10    // Second query
11    'feed({"offset":"10","limit":"10","category":"SPORTS"})': [
12      {
13        '__ref': 'FeedItem:11'
14      },
15      // ...additional items...
16    ]
17  }
18}

You can override this default behavior by defining a cache field policy for a particular field:

JavaScript
1const cache = new InMemoryCache({
2  typePolicies: {
3    Query: {
4      fields: {
5        feed: {
6          keyArgs: ["category"],
7        },
8      },
9    },
10  },
11});

This field policy for Query.feed includes a keyArgs array, which contains the names of all arguments that the cache should include in its storage keys.

In this case, we don't want the cache to treat offset or limit as key arguments, because those arguments don't change which list we're fetching from. However, we do want to treat category as a key argument, because we want to store our SPORTS feed separately from other feeds (such as FASHION or MUSIC).

After setting keyArgs as shown, we end up with a single cache entry for our SPORTS feed (note the absence of offset and limit in the storage key):

JavaScript
1{
2  'ROOT_QUERY': {
3    'feed({"category":"SPORTS"})': [
4      {
5        '__ref': 'FeedItem:1'
6      },
7      // ...additional items from first query...
8      {
9        '__ref': 'FeedItem:11'
10      },
11      // ...additional items from second query...
12    ]
13  }
14}

Important: After you define keyArgs for a paginated list field like Query.feed, you also need to define a merge function for the field. Otherwise, the list returned by the second query will overwrite the first list instead of merging with it.

Supported values for keyArgs

You can provide the following values for a field's keyArgs:

  • false (indicates that the field has no key arguments)

  • An array of argument, directive, and variable names

  • A function (advanced)

keyArgs array

A keyArgs array can include the types of values shown below. The storage key for a cached field uses the values of all arguments, directives, and variables included in the array.

  • Argument names:

    JavaScript
    1// Here, category and id are two arguments of the field
    2["category", "id"]
  • Nested argument names for input types with subfields:

    JavaScript
    1// Here, details is an input type argument
    2// with subfields name and date
    3["details", ["name", "date"] ]
  • Directive names (indicated with @), optionally with one or more of their arguments:

    JavaScript
    1// Here, @units is a directive that can be applied
    2// to the field, and it has a type argument
    3["@units", ["type"] ]
  • Variable names (indicated with $):

    JavaScript
    1// Here, $userId is a variable that's provided to some
    2// operations that include the field
    3["$userId"]

keyArgs function (advanced)

You can define a completely different format for a field's storage key by providing a custom function to keyArgs. This function takes the field's arguments and other context as parameters, and it can return any string to use as the storage key (or a dynamically-generated keyArgs array).

This is for advanced use cases. For details, see FieldPolicy API reference .

Which arguments belong in keyArgs?

When deciding which of a field's arguments to include in keyArgs, it's helpful to start by considering the two extremes: all arguments and no arguments. These initial options help to demonstrate the effects of adding or removing a single argument.

Using all arguments

If all arguments are key arguments (this is the default behavior), every distinct combination of argument values for a field results in a distinct cache entry. In other words, changing any argument value results in a different storage key, so the returned value is stored separately. We see this in our pagination example:

JavaScript
Cache
1{
2  'ROOT_QUERY': {
3    // First query
4    'feed({"offset":"0","limit":"10","category":"SPORTS"})': [
5      {
6        '__ref': 'FeedItem:1'
7      },
8      // ...additional items...
9    ],
10    // Second query
11    'feed({"offset":"10","limit":"10","category":"SPORTS"})': [
12      {
13        '__ref': 'FeedItem:11'
14      },
15      // ...additional items...
16    ]
17  }
18}

With this approach, Apollo Client can't return a cached value for a field unless all of the field's arguments match a previously cached result. This significantly reduces the cache's hit rate, but it also prevents the cache from returning an incorrect value when differences in arguments are relevant (as with our User example):

JavaScript
Cache
1{
2  'ROOT_QUERY': {
3    'user({"id":"1"})': {
4      '__ref': 'User:1'
5    },
6    'user({"id":"2"})': {
7      '__ref': 'User:2'
8    }
9  }
10}

Using no arguments

If no arguments are key arguments (you configure this by setting keyArgs: false), the field's storage key is just the field's name, without any argument values appended to it. This means that by default, whenever a query returns a value for that field, that value replaces whatever value was already in the cache.

This default behavior is often undesirable (especially for a paginated list), so you can define read and merge functions that use argument values to determine how a newly returned value is combined with an existing cached value.

Example

Recall this Query.feed field from Pagination issues :

GraphQL
1type Query {
2  feed(offset: Int, limit: Int, category: Category): [FeedItem!]
3}

We originally set keyArgs: ["category"] for this field to keep feed items from different categories separate. We can achieve the same behavior by setting keyArgs: false and defining the following read and merge functions:

JavaScript
1const cache = new InMemoryCache({
2  typePolicies: {
3    Query: {
4      fields: {
5        feed: {
6          keyArgs: false,
7
8          read(existing = {}, { args: { offset, limit, category }}) {
9            return existing[category]?.slice(offset, offset + limit);
10          },
11
12          merge(existing = {}, incoming, { args: { category, offset = 0 }}) {
13            const merged = existing[category] ? existing[category].slice(0) : [];
14            for (let i = 0; i < incoming.length; ++i) {
15              merged[offset + i] = incoming[i];
16            }
17            existing[category] = merged;
18            return existing;
19          },
20        },
21      },
22    },
23  },
24});

With the code above, the value of the existing cached value passed to our read and merge functions is a map of category names to FeedItem lists. This map enables our single cached field value to store multiple distinct lists. This manual separation is logically equivalent to using keyArgs: ["category"], so the extra effort is often unnecessary.

If we know that feeds with different category values have different data, and we know that our read function never needs simultaneous access to multiple category feeds, we can safely shift the responsibility for the category argument to keyArgs. This enables us to simplify our read and merge functions to handle only one feed at a time.

Summary

If the logic for storing and retrieving a field's data is identical for different values of a given argument (like category above), and the distinct field values are logically independent from one another, then you should probably add that argument to keyArgs to avoid handling it in your read and merge functions.

By contrast, arguments that limit, filter, sort, or otherwise reprocess existing field data usually do not belong in keyArgs. This is because putting them in keyArgs makes storage keys more diverse, reducing cache hit rate and limiting your ability to use different arguments to retrieve different views of the same data.

As a general rule, read and merge functions can do almost anything with your cached field data, but keyArgs often provide similar functionality with less code complexity. Whenever possible you should prefer the limited, declarative API of keyArgs over the unlimited power of functions like merge and read.

The @connection directive

The @connection directive is a Relay-inspired convention that Apollo Client supports. However, we recommend using keyArgs instead, because you can achieve the same effect with a single keyArgs configuration, whereas you need to include the @connection directive in every query you send to your server.

In other words, whereas Relay encourages the following @connection(...) directive for Query.feed queries:

JavaScript
1const FEED_QUERY = gql`
2  query Feed($category: FeedCategory!, $offset: Int, $limit: Int) {
3    feed(category: $category, offset: $offset, limit: $limit) @connection(
4      key: "feed",
5      filter: ["category"]
6    ) {
7      edges {
8        node { ... }
9      }
10      pageInfo {
11        endCursor
12        hasNextPage
13      }
14    }
15  }
16`;

in Apollo Client, you can use the following query (the same query without the @connection(...) directive):

JavaScript
1const FEED_QUERY = gql`
2  query Feed($category: FeedCategory!, $offset: Int, $limit: Int) {
3    feed(category: $category, offset: $offset, limit: $limit) {
4      edges {
5        node { ... }
6      }
7      pageInfo {
8        endCursor
9        hasNextPage
10      }
11    }
12  }
13`;

and instead configure keyArgs in your Query.feed field policy:

JavaScript
1const cache = new InMemoryCache({
2  typePolicies: {
3    Query: {
4      fields: {
5        feed: {
6          keyArgs: ["category"],
7        },
8      },
9    },
10  },
11})

If the Query.feed field does not have an argument like category that you can use in keyArgs: [...], then it might make sense to use the @connection directive after all:

JavaScript
1const FEED_QUERY = gql`
2  query Feed($offset: Int, $limit: Int, $feedKey: String) {
3    feed(offset: $offset, limit: $limit) @connection(key: $feedKey) {
4      edges {
5        node { ... }
6      }
7      pageInfo {
8        endCursor
9        hasNextPage
10      }
11    }
12  }
13`;

If you execute this query with different values for the $feedKey variable, those feed results are stored separately in the cache, whereas normally they would all be stored in the same list.

When choosing a keyArgs configuration for this Query.feed field, you should include the @connection directive as if it were an argument (the @ tells InMemoryCache you mean a directive):

JavaScript
1const cache = new InMemoryCache({
2  typePolicies: {
3    Query: {
4      fields: {
5        feed: {
6          keyArgs: ["@connection", ["key"]],
7        },
8      },
9    },
10  },
11})

With this configuration, your cache uses a feed:{"@connection":{"key":...}} key instead of just feed to store separate { edges, pageInfo } objects within the ROOT_QUERY object:

JavaScript
1expect(cache.extract()).toEqual({
2  ROOT_QUERY: {
3    __typename: "Query",
4    'feed:{"@connection":{"key":"some feed key"}}': { edges, pageInfo },
5    'feed:{"@connection":{"key":"another feed key"}}': { edges, pageInfo },
6    'feed:{"@connection":{"key":"yet another key"}}': { edges, pageInfo },
7    // ...
8  },
9})

The ["key"] in keyArgs: ["@connection", ["key"]] means only the key argument to the @connection directive is considered, and any other arguments (like filter) are ignored. Passing just key to @connection is usually adequate, but if you want to pass a filter: ["someArg", "anotherArg"] argument as well, you should instead include those argument names directly in keyArgs:

JavaScript
1const cache = new InMemoryCache({
2  typePolicies: {
3    Query: {
4      fields: {
5        feed: {
6          keyArgs: ["someArg", "anotherArg", "@connection", ["key"]],
7        },
8      },
9    },
10  },
11})

If any of these arguments or directives are not provided for the current query, they're omitted from the field key automatically, without error. This means it's generally safe to include more arguments or directives in keyArgs than you expect to receive in all cases.

As mentioned above, if a keyArgs array is insufficient to specify your desired field keys, you can alternatively pass a function for keyArgs, which takes the args object and a { typename, field, fieldName, variables } context parameter. This function can return either a string or a dynamically-generated keyArgs array.

Although keyArgs (and @connection) are useful for more than just paginated fields, it's worth noting that relayStylePagination configures keyArgs: false by default. You can reconfigure this keyArgs behavior by passing an alternate value to relayStylePagination:

JavaScript
1const cache = new InMemoryCache({
2  typePolicies: {
3    Query: {
4      fields: {
5        feed: relayStylePagination(["type", "@connection", ["key"]]),
6      },
7    },
8  },
9})

In the unlikely event that a keyArgs array is insufficient to capture the identity of a field, remember that you can pass a function for keyArgs, which allows you to serialize the args object however you want.