Update caches for many queries after one mutation

Hi,

Disclaimer: I’m new to GraphQL so I’m looking for advice from experts. :blush:

In my React application, I do the following:

  • Retrieve some variations of a Person object all over the application, through slightly different queries.
  • The object always contains a list of Tags
  • In one component, I create a new Tag and apply it via a mutation to a specific Person. Now the Person, in the back-end contains an extra tag in its Tags list.

In details:

  • Home page: queryPersonsWithSomeTags
  • Home page: queryAllTags
  • Person page: queryPersonWithAllTags
  • Another page: queryTagsThatBelongsToListOfPersons
  • Another page: mutationCreateTag
  • Another page: mutationApplyTagToListOfPersons

InAnother page, I use the mutation’s mutationApplyTagToListOfPersons “update” function to update Apollo’s cache but that is not scalable as I need to update the cache of every single query that returned the tag object.

Is updating the cache of various queries expected or it is most likely a misuse of the library or a mis-design of the application/queries where this case should not happen?

Thanks for the help and for the great library!
Nico

Apollo’s InMemoryCache is normalized, which means that even if you request the same Person multiple times but with different fields across your application, in all of these cases the same Person object will be referenced in the cache. This means that if you fire off a mutation that changes a field on this particular Person (tags in your example), you will have to perform only a single cache update:

mutationApplyTagToListOfPersons({
    update: (cache, { data }) => {
        cache.modify({
            id: 'the cache id of your Person object you wish to modify',
            fields: {
                tags: (existingTags, { toReference }) => (
                    [...existingTags, toReference(data.mutationApplyTagToListOfPersons)]
                )
            }
        });
    }
});

Hello! Hope you don’t mind me chiming in.

Is there any reason why the normalization would fail?
In one section of my app, shipments can be marked as hidden or not (isHidden). The shipments appear in a list that can be filtered and sorted, and of course manually modifying the cache for each individual combination of filters and sort fields would be unsustainable.

I have a custom mutation toggleHide which accepts one or several shipments and flips the isHidden value.

The following handler runs after the mutation. Individual shipments reflect the updated cache correctly. But as soon as I change the filters or sort fields I am seeing stale cache. I am not seeing any console errors anywhere.

const handleAfterToggleHide = function (cache, payload) {
      payload.data.toggleHide.map((modifiedShipment) =>
        cache.modify({
          id: cache.identify(modifiedShipment),
          fields: {
            // Toggle value of isHidden to its opposite
            isHidden(cachedValue) {
              return !cachedValue;
            },
          },
        })
      );
  };

Any idea why this would work for shipments that are currently visible on the screen, but not work for any other queries?

The only way I can imagine normalization not to work in this scenario, is if each permutation of filters stores different Shipment objects. Not sure how that can happen without seeing more of your code (what do the graphql queries look like?) and/or the contents of your cache. For the latter, you can check out this Chrome extension and see whether or not the same Shipment objects are referenced throughout the cache.

The query to list the shipments is pretty basic, see below. I see what you are saying about each permutation being different but I agree that sounds very unlikely, unless I am missing something here.

export const ALL_SHIPMENTS_QUERY = gql`
  query ALL_SHIPMENTS_QUERY(
    $orderBy: String
    $showHidden: Boolean
    $userId: ID!
  ) {
    allShipments(
      orderBy: $orderBy
      where: {
        createdBy: { id: $userId }
        { isHidden: $showHidden }
      }
    ) {
      id
      isHidden
      createdBy {
        name
      }
     // ... more fields
}

The only thing that occurs to me is that the custom mutation messes this up somehow, but I don’t see how, since I’m able to identify the cached object correctly.

This is the mutation (frontend):

mutation TOGGLE_HIDE_MUTATION($shipmentIds: [ID!]) {
    toggleHide(shipmentIds: $shipmentIds) {
      id
    }
  }

The mutation is executed in a Keystone custom mutation resolver and this is the return value:


return await context.lists.Shipment.updateMany({
    data: updatedShipments, // shipments with .isHidden updated
    resolveFields: graphql`
      id
    `,
  });

Does anything jump out to you at all?

The one thing that does jump out to me is that your isHidden field does not seem to be referenced anywhere. You’re modifying it with your cache.modify calls, but it’s present in neither your query nor your mutation. If that’s the case, I would actually expect your cache.modify calls to not do anything at all because they can only modify fields that are already in the cache - and if your query never fetched isHidden, it won’t be in the cache. I find it odd that you’re noticing changes in your shipments at all.

That said, if you did reference isHidden in both your query and your mutation, Apollo should be able to perform the cache updates automatically (making your cache.modify calls unnecessary), because the mutation will return a list of objects containing id and __typename fields which are already in the cache.

Hello! I have updated my ALL_SHIPMENTS_QUERY above, which was previously edited for brevity. isHidden is one of the fields I am selecting, and it is also part of the where clause.

However I don’t reference isHidden in the mutation, instead I just pass the ID as argument and toggle the field inside a mutation resolver in the backend. For this reason I see why cache.modify is necessary, since the field is changed without Apollo’s knowledge.

Is it possible that I have to use a non-custom mutation for cache to work? I’m not sure why, since I already am able to use cache.identify + cache.modify and it’s partially working.