Automatically Update Apollo Cache after Mutations
Tags: apollo, graphql, offix, aerogear
Published: 2019-06-13 11:40:44

### Background

The application cache provides developers with a complete local state and the ability to work seamlessly in offline situations. It also allows us to minimise the load on our server. However, with this increased power we inherit increased complexity and corner cases where our application may no longer perform as expected, possibly containing outdated and irrelevant data.

In [Apollo Client 2.0](https://www.apollographql.com/docs/react/) developers can use the [InMemoryCache](https://www.apollographql.com/docs/react/advanced/caching/#inmemorycache) package to provide their client with a fully featured cache solution. This cache can be used to resolve data locally or to reduce the number of calls to your server when you do not need to replicate everything the server knows about. The cache is normalised and provides a GraphQL API under which Queries and Mutations are stored. This is a developer-friendly way to organise data.

A high-level diagram of a typical Apollo Client cache can be seen below.

![Apollo Cache Diagram](https://thepracticaldev.s3.amazonaws.com/i/58ww58lwyz8w9z0gpnw5.png)

In the above example, we see that the Apollo Client queries or modifies data from one of two places. Depending on how you have configured your client it will either check the cache first to see if it contains the data you are or looking for, or it will ask the server and then tell the cache about the result. This is the foundation of implementing an offline application where your client can now query and mutate data locally without interacting with a server.

Apollo Client by default tries to keep its cache up to date. As an example, let's pretend we have a `getItems` Query in our cache which contains a list of items and each of these items has a title field. If we (through a mutation) update the title field of one of these items, then Apollo will ensure that our `getItems` Query is also updated. This ensures that the next time we run `getItems` (if, for example, you have a slow or non-existent network connection) we get the most up to date information.

### The Problem

This trivial example is handled quite nicely by Apollo. However, this is because Apollo already knows about this object we have updated. What if it is a new object we have just created? Or if we delete an object and need to dereference it everywhere? These cases are a little less trivial. When I began working with Apollo Client and the cache I immediately noticed some common pain points associated with dealing with these use cases. Let's explore how these more complex scenarios are handled and why doing it inside my own application led me to create a generic package which the community could benefit from.

### Manual Cache Updates

To handle anything outside of basic cache updates, Apollo gives us the ability to provide our client with custom update functions. If we use the basic "ToDo" app example and we want to add a newly created task to our cache, the code will look something like the following:

```javascript
apollo.mutate({
  mutation: createTaskMutation,
  variables: item,
  update: (cache, { data }) => {
    try {
      let { allTasks } = cache.readQuery({ query: getTasks });
      allTasks.push(data);
      cache.writeQuery({
        query: getTasks,
        data: {
            'allTasks': allTasks
        }
      });
    } catch (e) {
        // We should always catch here,
        // as the cache may be empty or the query may fail
    }
});
```

Let's walk through this example and explain what is happening. We have a (prebuilt) mutation named `createTaskMutation`, and our `update` function which accepts a `cache` object and also the returned `data` object from the result of our mutation. This function then attempts to do three things:

1. Read the prebuilt `getTasks` Query from our cache.
1. Push the new `data` object to our current list of tasks
1. Write that query back to the cache with the new data now contained within.

Once this `update` function has run we will have our updated data locally in our cache. The perfect solution! Not really.

### Introducing Complexity

Now that we know how the `update` function works with a trivial case we can explore something more realistic. What about when we introduce subscriptions or multiple queries that need to be updated?

#### Subscriptions

If our application is also subscribed to `getTasks` it is quite possible that the subscription data will return before the result of the mutation. This means the result of the mutation we created could already be in our cache. This instantly makes the above `update` function much more complicated, as in its current state our `allTasks` object in our cache will now contain our new task twice. In other words, we need to make sure deduplication is performed on our cache.

We need to adapt the `update` function to the following:

```javascript
apollo.mutate({
  mutation: createTaskMutation,
  variables: item,
  update: (cache, { data }) => {
    try {
      let { allTasks } = cache.readQuery({ query: getTasks });
      if (allTasks) {
        if (!allTasks.find(task => task.id === data.id)) {
          allTasks.push(data);
        }
      } else {
        allTasks = [data];
      }
      cache.writeQuery({
        query: getTasks,
        data: {
            'allTasks': allTasks
        }
      });
    } catch (e) {
        // We should always catch here,
        // as the cache may be empty or the query may fail
    }
});
```

As you can see, we read from the cache but crucially we iterate through its contents before making any changes. If we don't find the result of our mutation already in our cache then we can safely add it.

#### Multiple Queries

Another problem is when there are multiple Queries in our cache which need to be updated. This can happen when several different Queries return subsets of the same data. For example, if a different Query named `userTasks` also needs to be updated when a task is added the code will resemble something similar to the following:

```javascript
apollo.mutate({
  mutation: createTaskMutation,
  variables: item,
  update: (cache, { data }) => {
    try {
      let { allTasks } = cache.readQuery({ query: getTasks });
      if (allTasks) {
        if (!allTasks.find(task => task.id === data.id)) {
          allTasks.push(data);
        }
      } else {
        allTasks = [data];
      }
      cache.writeQuery({
        query: getTasks,
        data: {
            'allTasks': allTasks
        }
      });
      let { userTasks } = cache.readQuery({ query: userTasks });
      if (userTasks) {
        if (!userTasks.find(task => task.id === data.id)) {
          userTasks.push(data);
        }
      } else {
        userTasks = [data];
      }
      cache.writeQuery({
        query: userTasks,
        data: {
            'userTasks': userTasks
        }
      });
    } catch (e) {
        // We should always catch here,
        // as the cache may be empty or the query may fail
    }
});
```

In the above example we have two queries we need to update, both of which should contain our new ToDo item. Our update function is now becoming incredibly obtuse and is filled with a lot of boilerplate code. Keep in mind that all of this boilerplate is for _one_ mutation. What about when our application contains several mutations and this boilerplate needs to be placed in several different places?

### Out of the Box Cache Helpers

Instead of requiring this boilerplate with essentially the same basic three steps each time of _read, append, write_, I thought it would be a good idea to extract the logic to a separate helper package. These helpers provide a way to generate mutation options in a way that is compatible with Apollo's mutate API. This means for very little overhead developers get the above `update` function built automatically for any mutation they wish.

Let's take a look at how we can use one of these functions to make sure the cache is kept completely up to date after a mutation. First, we need to install the package:

```bash
npm i offix-cache
```

We can then use it within our application like so:

```javascript
const { createMutationOptions } = require('offix-cache');

const mutationOptions = {
  mutation: createTaskMutation,
  variables: item,
  updateQuery: {
    query: getTasks
  }
};

const generatedOptions = createMutationOptions(mutationOptions);

apollo.mutate(generatedOptions);
```

In the example above we need to provide the Query we want updated (in this case `getTasks`) when the mutation result returns. The `generatedOptions` object returned then contains an `update` function which automatically does the deduplication we saw in a previous code example.

Extending this a bit further - what about if, like we previously mentioned, we have several Queries we want to update once our mutation result returns? The API also accepts an array of Queries, so we can pass any which need to be updated. The `mutationOptions` object then looks like the following:

```javascript
const mutationOptions = {
  mutation: createTaskMutation,
  variables: item,
  updateQuery: [
    {
      query: getTasks
    },
    {
      query: userTasks,
      variables: { user : 1 }
    }
  ]
};
```

Once we pass this to `createMutationOptions` we will then have a set of mutation options where our `update` function will automatically update several Queries upon receiving a result, all for very low overhead.

In a later post, I will also detail some other nice features which may be of use, like automatic optimistic response generation and subscription helpers. For now, if you would like to see some more advanced use-cases please take a look at the project's [readme](https://github.com/aerogear/offix/tree/master/packages/offix-cache). Or test it out on [npm](https://www.npmjs.com/package/offix-cache).