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