apollo-link-watched-mutation
A Link interface for providing default cache updates based on { mutation : query } relationships
This is a maintained fork of afafafafafaf/apollo-link-watched-mutation.
Setup
npm i @kiwee/apollo-link-watched-mutation
(peer dependencies)
# works with @apollo/client 3 and graphql 14, 15, 16
npm i @apollo/client 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.
- 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.
- 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.
- 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
NOTE: the examples haven't been updated to the newest version of the package.
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