Results for different keyArgs value queries are getting passed to/mixed up in merge type policy

I have a pagination query that takes in an offset, limit, and category field, the last of which is used to filter results by a certain type. I’ve implemented the type policy for this query using the default offsetLimitPagination helper as mentioned here, and infinite scroll within the same query does seem to be working.

In my app, this query is used to display a list of results for a particular category, with an option at the top of the screen to switch between categories (active and expired). I’ve specified category as a keyArg, but despite this, when I switch between categories, I see the results of the previous category at the top of my new list (i.e. if active has 5 results and expired has 3, switching from active → expired will show 8 results instead of 3)

When using the actual source for the merge function from above, I’m seeing that existing is populated with the prior query’s results, even when the category keyArg changes. I was under the impression that if the keyArg values change, they’re stored separately, in which case any change in keyArg should result in existing being undefined. Am I fundamentally misunderstanding how this works?

If that’s not how it’s supposed to work, how can I modify the merge such that it only merges the results for non-keyArg differences (limit and offset)?

Hey @johnnywang :wave:

This seems suspect and I agree I wouldn’t expect this to happen. Would you be able to provide a small runnable example, or paste what the contents of your cache are? You can either use Apollo’s DevTools or spit out a JS object using client.cache.extract(). I’m curious if your cache keys are the same or different in the cache itself for the paginated data.

Hey @jerelmiller!

A runnable example would be difficult, unfortunately. Extracting the cache, it does appear that each individual item is cached under different keys:

Here’s what the merge function gets when the (React Native) screen first queries against the active filter:

 LOG  -----------args {"input": {"category": "ACTIVE", "limit": 8, "offset": 0}}
 LOG  -----------existing undefined
 LOG  -----------incoming [{"__ref": "GroupOffer:fa7a5617-0bc6-46fd-bef3-395661538c44"}, {"__ref": "GroupOffer:f73fb306-0250-4805-b1d1-890d3c6de960"}, {"__ref": "GroupOffer:96ef3c51-aa02-4118-aa4c-18fa754ad122"}, {"__ref": "GroupOffer:372cc59c-8a53-456a-9e93-beb3017b4f80"}, {"__ref": "GroupOffer:fc1be3a5-baa9-4fff-a48b-d637130e5008"}]

Which corresponds to the cache values here:

GroupOffer:96ef3c51-aa02-4118-aa4c-18fa754ad122: { ...offerDetails }
GroupOffer:372cc59c-8a53-456a-9e93-beb3017b4f80: { ...offerDetails }
GroupOffer:f73fb306-0250-4805-b1d1-890d3c6de960: { ...offerDetails }
GroupOffer:fa7a5617-0bc6-46fd-bef3-395661538c44: { ...offerDetails }
GroupOffer:fc1be3a5-baa9-4fff-a48b-d637130e5008: { ...offerDetails }

When I switch over to the inactive filter, this is what merge gets (notice that the existing is populated and set to the previous active cache keys):

 LOG  -----------args {"input": {"category": "INACTIVE", "limit": 8, "offset": 0}}
 LOG  -----------existing [{"__ref": "GroupOffer:fa7a5617-0bc6-46fd-bef3-395661538c44"}, {"__ref": "GroupOffer:f73fb306-0250-4805-b1d1-890d3c6de960"}, {"__ref": "GroupOffer:96ef3c51-aa02-4118-aa4c-18fa754ad122"}, {"__ref": "GroupOffer:372cc59c-8a53-456a-9e93-beb3017b4f80"}, {"__ref": "GroupOffer:fc1be3a5-baa9-4fff-a48b-d637130e5008"}]
 LOG  -----------incoming [{"__ref": "GroupOffer:3ce37911-8a6b-4bfa-937b-5481149e6579"}, {"__ref": "GroupOffer:7630f3c6-130a-4bf5-9155-341f7047e325"}, {"__ref": "GroupOffer:f99025f2-689b-4aca-b546-61e4fa45ae5c"}, {"__ref": "GroupOffer:ecb621a2-576b-4d8b-9623-d538fdb4ad08"}, {"__ref": "GroupOffer:41005df3-cafa-4c76-8582-6508343a8e3f"}, {"__ref": "GroupOffer:e7edae04-5460-43af-acc9-e0476ea8d996"}, {"__ref": "GroupOffer:2af859a8-4cde-438e-9d99-18dfa936d76d"}, {"__ref": "GroupOffer:379109a1-d666-4077-b89c-ae35d1280cd1"}]

And in the cache itself you see the old and new keys:

GroupOffer:2af859a8-4cde-438e-9d99-18dfa936d76d: { ...offerDetails }
GroupOffer:3ce37911-8a6b-4bfa-937b-5481149e6579: { ...offerDetails }
GroupOffer:96ef3c51-aa02-4118-aa4c-18fa754ad122: { ...offerDetails }
GroupOffer:372cc59c-8a53-456a-9e93-beb3017b4f80: { ...offerDetails }
GroupOffer:7630f3c6-130a-4bf5-9155-341f7047e325: { ...offerDetails }
GroupOffer:41005df3-cafa-4c76-8582-6508343a8e3f: { ...offerDetails }
GroupOffer:379109a1-d666-4077-b89c-ae35d1280cd1: { ...offerDetails }
GroupOffer:e7edae04-5460-43af-acc9-e0476ea8d996: { ...offerDetails }
GroupOffer:ecb621a2-576b-4d8b-9623-d538fdb4ad08: { ...offerDetails }
GroupOffer:f73fb306-0250-4805-b1d1-890d3c6de960: { ...offerDetails }
GroupOffer:f99025f2-689b-4aca-b546-61e4fa45ae5c: { ...offerDetails }
GroupOffer:fa7a5617-0bc6-46fd-bef3-395661538c44: { ...offerDetails }
GroupOffer:fc1be3a5-baa9-4fff-a48b-d637130e5008: { ...offerDetails }

And this is what the relevant portion of my field policy looks like:

      groupOfferQuery: {
        keyArgs: ['category'],
        merge(existing, incoming, { args, cache }) {
          console.log('-----------args', args);
          console.log('-----------existing', existing);
          console.log('-----------incoming', incoming);
          console.log('-----------cache', cache.extract());

          const merged = existing ? existing.slice(0) : [];

The UI does seem to be “working” now that I switched from cursor to offset pagination, but I suspect it’s only working because the offset is reset back to 0, and is overriding the sliced existing array. With cursor style, since there’s no overwrite, you end up getting old and new results.

Hey @johnnywang

The thing that sticks out to me right away is that the category arg is tucked under the input key. I’m willing to bet that the key arg thats being calculated for that field is undefined instead of the category value ACTIVE or INACTIVE in this case, so its likely the cache is thinking you’re trying to calculate a list from the same value (undefined). This would explain why you’re seeing the same set of data in both places.

If I recall correctly, you should be able to declare keyArgs similar to keyFields where you can pass a nested array to get a field inside an object. In your case, this should look like this:

keyArgs: ['input', ['category']]

If you’re using the offsetLimitPagination helper, the keyArgs value is the argument to that function, so it would look like this.

offsetLimitPagination(['input', ['category']])

Try this and see if it helps!

Oh that did it, thanks @jerelmiller! That thought crossed my mind very briefly, since I had to revise the pagination helper to account for offset not being at the top level either:

        // Modify Apollo's default offsetLimitPagination implementation since
        // we nest our args under an `input` key
        // https://github.com/apollographql/apollo-client/blob/main/src/utilities/policies/pagination.ts#L33-L49
        merge(existing, incoming, { args }) {
          const merged = existing ? existing.slice(0) : [];

          if (incoming) {
            if (args) {
              const { offset = 0 } = args.input;

I’m assuming there’s no way to get around that short of just copying and modifying the source like I’m doing here?

But also, that might be a worthwhile note to add to the documentation, to call out that these only work under the expectation of a certain top-level format

Oh thats a good point that the default implementation of offsetLimitPagination assumes limit and offset are top-level args. Really good call on updating the documentation to mention that this utility is designed for the “common” case but you’d need to copy the implementation to account for differences. Copying the source and modifying it like you’re doing is unfortunately the best way to get this working properly in your case.

Glad to hear this fixed the issue for you!