apollo-feature-requests icon indicating copy to clipboard operation
apollo-feature-requests copied to clipboard

Add support for DocumentNode to cache.evict()

Open ValentinH opened this issue 1 year ago • 2 comments

First, the issue I'm trying to address with this feature request was perfectly stated in 2016 in this comment: image

Today, when we have a mutation that is affecting the result of a query done on another page, I'm using one of these approach:

  • setting fetchPolicy to cache-and-network: not ideal because you are over-fetching most of the time
  • using refetchQueries on the mutation: not ideal either for the reasons mentioned in #419 + because when using different set of variables (like when doing pagination), it only refetched that most recently used set of variables. In addition, sometimes we just don't want to refetch because the user might not go back to the query page so it could also be over-fetching.
  • using updateQuery from the query and manually update the cache: most performant way to do it but it sometimes super hard or even impossible if the query is reading from an aggregation table for example.

What I'm looking for is a way to invalidate a set of queries similar to react-query invalidateQueries.

I started to use cache.evict({ fieldName: 'example' }) but the issue is that the fieldName isn't type-safe. Therefore, I built this helper function to do this in a safe way from an array of DocumentNode (generated by graphql-codegen):

const invalidateQueries = (cache: ApolloCache<any>, documentNodes: DocumentNode[]) => {
  const fieldsToInvalidate = documentNodes.flatMap((documentNode) =>
    documentNode.definitions.flatMap((definition) => {
      if (!('selectionSet' in definition)) {
        return [];
      }
      return definition.selectionSet.selections
        .map((selection) => {
          if ('name' in selection) {
            return selection.name.value;
          }
          return null;
        })
        .filter(isPresent);
    })
  );
  fieldsToInvalidate.forEach((fieldName) => {
    cache.evict({
      fieldName,
    });
  });
  // I'm not yet sure if I should call `cache.gc()` here 
};

I can then use it like this:

  const [exampleMutation, result] = useMutation(ExampleMutationDocument);

  const doMutation = async () => {
    await exampleMutation({
      variables: {
        value: 'whatever'
      },
      update: (cache) => {
        invalidateQueries(cache, [ExampleQueryDocument]);
      },
    });
  };

This is working fine on the tests I did but now I'm wondering if cache.evict() could directly accept a DocumentNode (or even an array of nodes) to evict all the fields selected by the queries (this comment was also suggesting this).

Basically being able to do:

  const [exampleMutation, result] = useMutation(ExampleMutationDocument);

  const doMutation = async () => {
    await exampleMutation({
      variables: {
        value: 'whatever'
      },
      update: (cache) => {
        cache.evict(ExampleQueryDocument)
      },
    });
  };

or to be even closer to react-query:

 const [exampleMutation, result] = useMutation(ExampleMutationDocument);

  const doMutation = async () => {
    await exampleMutation({
      variables: {
        value: 'whatever'
      },
      invalidateQueries: [ExampleQueryDocument]
    });
  };

ValentinH avatar Jun 21 '24 12:06 ValentinH