Custom cache key function for ROOT_QUERY

I’m trying to configure InMemoryCache so that ROOT_QUERY has the same key whether a variable is null or undefined (or not passed at all, but that seems to be the same as undefined).

I’ve tried to use typePolicies:

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      keyFields: (object, context) => ...
    }
  }
});

but when logging this function never seems to be called. RootQuery or ROOT_QUERY instead of Query don’t work either.

Is this possible? Or would I need to subclass InMemoryCache instead?

Now I see iOS - How to take query's input out of cache key, which suggests subclassing is the only solution, or at least was in April 2023. On the other hand, cache.identify for ROOT_QUERY · Issue #9377 · apollographql/apollo-client · GitHub mentions

Unless you’re defining a custom keyFields array or dataIdFromObject function for the Query type

which suggests it is possible.

Hey @Alexey_Romanov :wave:

Could you explain a little further what you mean by this?

I’m trying to configure InMemoryCache so that ROOT_QUERY has the same key whether a variable is null or undefined

ROOT_QUERY doesn’t have variables applied to it (in other words, ROOT_QUERY is not a field itself) The cache treats root typenames a bit differently. Perhaps Ben was hinting at something different in that comment?

Are you trying to access a particular field on that root query object? Would you be able to provide a bit more information about what you’re trying to accomplish in terms of your schema/query?

would I need to subclass InMemoryCache instead?

We don’t recommend this unless you have a very good reason to do so. Updates we make to this class have the potential to break custom subclasses as the internal implementation changes. The only “blessed” subclass option is to inherit from Cache which is designed as an abstract class with some base functionality for anyone wanting to create their own cache implementation. Doing so though means you lose out on all the InMemoryCache functionality.

Let’s see if we can find a solution that doesn’t require you to do this.

Let’s say I have a

query myQuery($var1: Int!, $var2: Int) { ... }

and the client code makes two queries:

useQuery(MyQueryDocument, variables: { var1: 0, var2: null })
useQuery(MyQueryDocument, variables: { var1: 0 })

In the actual case this is a very large project and the queries would be in different files and hard to find.

Then in the cache I see

      ROOT_QUERY: [Object: null prototype] {
        __typename: 'Query',
        'myQuery({"var1":1})': { __ref: 'Result:1' },
        'myQuery({"var1":1,"var2":null})': { __ref: 'Result:1' },
      }

I want to end up with only one of those entries (it doesn’t matter which), and for the second query to use the first one’s entry.

You might be interested in keyArgs here on the myQuery field inside the Query type so that you can combine those into the same cache entry. From the docs:

By default, all of a field’s arguments are key arguments. This means that the cache stores a separate value for every unique combination of argument values you provide when querying a particular field .

If you specify a field’s key arguments, the cache understands that the rest of that field’s arguments aren’t key arguments. This means that the cache doesn’t need to store a completely separate value when a non-key argument changes.

For example, let’s say you execute two different queries with the monthForNumber field, passing the same number argument but different accessToken arguments. In this case, the second query response will overwrite the first, because both invocations use an identical value for the only key argument.

From what you’ve provided here, my guess is var1 is the thing that makes this field “unique” among its arguments so I’d use that as your key argument.

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        myQuery: {
          keyArgs: ['var1']
        }
      }
    }
  }
})

This would write both of those queries into the cache as such:

 ROOT_QUERY: [Object: null prototype] {
    __typename: 'Query',
    'myQuery({"var1":1})': { __ref: 'Result:1' },
  }

If you need a bit more control over how that value is read out of the cache with var2 in mind, you might also want/need to combine this with a read function.

See if this helps!

There are two issues here:

  1. I want this to happen for all queries, not just myQuery. The server guarantees it never cares about the difference between null and undefined. This could be solved by introspection, but it’s turned off in production. I guess there are workarounds to generate the list of all queries, but it still seems ugly.
  2. var2 should also be included if it isn’t null. This is probably easy to solve by providing a function instead of an array to keyArgs.
  1. Unfortunately you’ll need to define keyArgs for all fields that you want here. To my knowledge, there is nothing out of the box in Apollo that would let you apply this universally. That being said, you can always create a shared function that you can set on any query field that needs this behavior:

  2. Ah good to know. Then yes, a keyArgs function should suffice here.

Combining these, something like this might work (warning, untested):

import type { KeyArgsFunction } from '@apollo/client';

const defaultKeyArgs: KeyArgsFunction = (args) => {
  return Object.keys(args).filter((key) => args[key] !== null)
}

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        myQuery: { keyArgs: defaultKeyArgs },
        myQuery2: { keyArgs: defaultKeyArgs },
        myQuery3: { keyArgs: defaultKeyArgs },
        // etc
      }
    }
  }
})

Obviously feel free to modify this as you see fit, but this might help as a starting point.

1 Like

OK, thank you. I will try this approach.

1 Like

Yes, it works. I used Named Operations Object (GraphQL-Codegen) to get the list of queries.

There is a minor detail that using keyArgs changes the key format: I get myQuery:{"var1":1} instead of myQuery({"var1":1}). But it doesn’t matter for my use-case.

The issue I have now is that we have many queries, enough that generating a list of them all increases bundle size by over 1 Kb, and makes performance worse according to our tests. I may be able to limit to the non-admin queries, but that’s less trivial, and at any rate leaves us with a size increase.

So it seems we may need to try subclassing InMemoryCache after all (or Cache), or contributing to fix

there is nothing out of the box in Apollo that would let you apply this universally