apollo-link-watched-mutation

An interface for providing default updates for mutation - query pairings

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
apollo-link-watched-mutation
115100.2.44 years ago5 years agoMinified + gzip package size for apollo-link-watched-mutation in KB

Readme

apollo-link-watched-mutation GitHub license PRs Welcome
A Link interface for providing default cache updates based on { mutation : query } relationships

Setup

npm i -S apollo-link-watched-mutation

(peer dependencies)
npm i -S apollo-link graphql

Why does this package exist?

Background

Easy client-side caching is one of the many reasons the Apollo Client has gained so much popularity.
One of the best features of the apollo-client is the idea of automatic cache updates. Since the apollo-client handles your networking for you, it can inspect traffic and update its cache in-place if it ever sees an updated version of a cached value in a response.
However, there are scenarios (e.g. item creation, deletion, filtered lists, etc.) where an in-place update may not be sufficient.
Now, Apollo also offers some ways around this, notably in the form of an update variable, which provides direct-access to the apollo-cache.
However, this update variable doesn't scale very well (in terms of developer maintainability) in a couple scenarios.
  1. As the number of queries to update grows, you need knowledge of all of their cache keys in order to keep them updated. In the case of variable lists which aren't predefined, this means you also need to store all of their variables in order to read and update their cached values.
  2. As the number of places where you call the same or similar mutations grows, you need to replicate or find a good general pattern to recreate all of the cacheKey storing and updating logic described above.
  3. You need to, to some extent, embed this logic inside your components, conflating your view with your caching layer even though you could declare the caching behavior you want on start-up.
// This update becomes a lot more complex if...
// 1. we have a variable # of cached filtered lists of todos based off status and other variables (instead of one predetermined list to update)
// 2. we need to conditionally add or remove todos based off these variables  (instead of always adding to the list)
update: (proxy, { data: { createTodo } }) => {
      const data = proxy.readQuery({
        query: TodoAppQuery,
        variables: { status: 'IN_PROGRESS' }
      });
      data.todos.push(createTodo);
      proxy.writeQuery({ query: TodoAppQuery, data });
},

The Link pattern for default cache updates

With Apollo-Client 2.0 the Apollo team introduced the idea of Apollo Links which provides a configurable way to compose your network stack. Apollo Links have access to the same network traffic that made Watched Queries possible and that means we can watch that same traffic and provide additional default update behavior to our cache.
new WatchedMutationLink(
  cache, // whatever is passed to the apollo-client
  {
    saveTodo: {
      TodoList: ({ mutation, query }) => { /* */ }
    }
  }
)
By adding this WatchedMutationLink to our networking stack, the exported apollo-client would invoke the callback provided for each cached query named TodoList whenever a successful mutation named SaveTodo is received. If the WatchedMutationLink receives an updated form of the cached data from the callback, it will write that updated data to the cache.
By monitoring networking traffic, the Link figures out when you may want to update your cache based off a mutation and query and you determine how it should update all in one place.

Example usage

The WatchedMutationLink below would manage any # of cached TodoLists after any successful SaveTodo mutation (including optimistic mutations & any reversions due to an error) by determining whether or not the mutatedTodo remains relevant on the list and updating the cache if necessary.
query TodoList($status: String) {
  todoList(status: $status) {
    id
    name
    status
  }
}
mutation SaveTodo(
  $id: String
  $status: String
) {
  saveTodo(
    id: $id
    status: $status
  ) {
    id
    name
    status
  }
}

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink } from 'apollo-link';
import WatchedMutationLink from 'apollo-link-watched-mutation';

const cache = new InMemoryCache();
const link = ApolloLink.from([
  new WatchedMutationLink(cache, {
    saveTodo: {
      TodoList: ({ mutation, query }) => {
        const mutatedTodoId = mutation.variables.id;
        const updatedTodo = mutation.result.data.saveTodo;
        const cachedTodos = query.result.todos.items;
        const isTodoIncluded = cachedTodos.some(todos => todos.id === mutatedTodoId);
        const shouldTodoBeIncluded = !query.variables || !query.variables.status || query.variables.status.includes(updatedTodo.status);

        let updatedItems = null;
        if (isTodoIncluded && !shouldTodoBeIncluded) {
          // if the mutatedTodo is found in the cached list and it should not be there after the mutation, remove it
          updatedItems = cachedTodos.filter(todo => todo.id !== mutatedTodoId);
        } else if (!isTodoIncluded && shouldTodoBeIncluded) {
          // if the mutatedTodo is not found in the cached list and it should be there after the mutation, add it
          updatedItems = [updatedTodo, ...cachedTodos];
        }
        // else an in-place update is already sufficient so no update is necessary
        if (updatedItems) {
          // immutably return query.result with updated items
          return {
            ...query.result,
            todos: {
              ...query.result.todos,
              items: updatedItems,
            },
          };
        }
      }
    }
  }),
]);
const client = new ApolloClient({ link, cache });
export default client;

Tests

npm test