Under heavy development
I wanted an updater that steals the magic of refetch queries while keeping the power of apollo local cache, but stripped of the boilerplate usually needed for each mutation update.
Updating the local cache becomes exponentially complicated when it needs to:
- include multiple variables
- include multiple queries
- know which of our target queries has been already fired before our speficific mutation happend
- cover scenarios** where apollo's in-place update may not be sufficient
** Add/remove to list, move from one list to another, update filtered list, etc.
This solution tries to decouple the view from the caching layer by configuring the mutation's result caching behavior through the Apollo's update
variable.
$ npm install --save apollo-cache-updater
OR
$ yarn add apollo-cache-updater
Example: Add an Article
The following block of code:
- adds a new article to getArticles queries that contain the
published: true
variable - adds
1
to the articleCounts queries that contain thepublished: true
variable
import ApolloCacheUpdater from "apollo-cache-updater";
import { getArticles, articlesCount } form '../../apollo/api'; // your apollo queries
createArticleMutation({ // your mutation
variables: {
...articleVariables // your mutation vars
},
update: (proxy, { data: { createArticle = {} } }) => { // your mutation response
const mutationResult = createArticle; // mutation result to pass into the updater
const updates = ApolloCacheUpdater({
proxy, // apollo proxy
queriesToUpdate: [getArticles, articlesCount], // queries you want to automatically update
searchVariables: {
published: true, // update queries in the cache that have these vars
},
mutationResult,
})
if (updates) console.log(`Article added`) // if no errors
},
})
Example: Remove an Article
The following block of code:
- removes an article with a specific id from the getArticles queries that contain the
published: true
variable - subtract
1
from the articleCounts queries that contain thepublished: true
variable
removeArticleMutation({ // your mutation
variables: {
id: article.id // your mutation vars
},
update: (proxy }) => {
const updates = ApolloCacheUpdater({
proxy, // mandatory
queriesToUpdate: [getArticles, articlesCount], // queries you want to automatically update
searchVariables: {
published: true, // update queries in the cache that have these vars
},
operation: 'REMOVE',
mutationResult: { id: article.id },
})
if (updates) console.log(`Article removed`) // if no errors
},
})
This package assumes an exact correspondence between operation's params and query's params. So the updates will not work if you're using queries like this:
query getItems(
$limit: Int
$offset: Int
$sort: String
) {
getItems(
options: {
limit: $limit
offset: $offset
sort: $sort
}
) {
...
}
It should be:
query getItems(
$limit: Int
$offset: Int
$sort: String
) {
getItems(
limit: $limit
offset: $offset
sort: $sort
) {
...
}
@client directive: remote queries with mixed local state.
In case you have queries like this one:
const GET_TODO = gql`
query todos {
todos {
id
type
local @client
}
}
`;
It's your duty to extend the mutation results with the local state fields that are going to be missing from the response:
update: (proxy, { data: { addTodo = {} } }) => {
// your mutation response
const mutationResult = addTodo; // mutation result to pass into the updater
const updates = ApolloCacheUpdater({
proxy, // apollo proxy
queriesToUpdate: [GET_TODO], // queries you want to automatically update
searchVariables: {},
mutationResult: {
...mutationResult,
local: client.localState.resolvers[
mutationResult.__typename
].local(mutationResult)
}
});
if (updates) console.log(`Todo added`); // if no errors
}
Check this fully working example here
Example: Match any query
If you need to go through all matching query names ignoring any variables you should use the ANY
operator. This does not work with the MOVE
operation.
The following code removes the article with the matching id from any getArticles cached items, despite all possibe variables combinations stored in the cache.
removeArticleMutation({ // your mutation
variables: {
id: article.id // your mutation vars
},
update: (proxy }) => {
const updates = ApolloCacheUpdater({
proxy, // mandatory
operator: 'ANY',
queriesToUpdate: [getArticles], // queries you want to automatically update
searchVariables: {},
operation: 'REMOVE',
mutationResult: { id: article.id },
})
if (updates) console.log(`Article removed`) // if no errors
},
})
Example: Move an Article
The following block of code:
- removes an article from getArticles queries that contain the
published: true
variable and adds it to getArticles queries that contain thepublished: false
variables - adds
1
to the articleCounts that contain thepublished: true
variable and adds it to articleCounts queries that contain thepublished: false
variables
setArticleStatus({
variables: {
_id: id,
published: false, // set the published status to false
},
update: (proxy, { data: { setArticleStatus = {} } }) => {
const mutationResult = setArticleStatus;
const updates = ApolloCacheUpdater({
proxy, // mandatory
operation: 'MOVE',
queriesToUpdate: [getArticles, articlesCount],
searchVariables: {
published: true, // find the mutation result article that in the cache is still part of the queries with published = true and remove it
},
switchVars: {
published: false, // add the mutation result article to the queries that in the cache were invoked with published = false, if any
},
mutationResult,
})
if (updates) console.log(`Article moved`)
},
})
Complete configuration object
{
proxy, // mandatory
searchOperator: 'AND', // [AND, AND_EDGE, OR, OR_EDGE, ANY], default AND. If you need to match all searchVariable, just one at least or none (with the latter will ignore any variables)
searchVariables: {
...vars // searchVariables cannot be nested objects
},
queriesToUpdate: [...queries],
operation: { // String || Object, default String ('ADD', 'REMOVE', 'MOVE', default: 'ADD')
type: 'MOVE', // 'ADD', 'REMOVE', 'MOVE', default: ADD
row: { // String || Object, default String ('TOP', 'BOTTOM', 'SORT', default: TOP)
type: 'SORT', // 'TOP', 'BOTTOM', 'SORT', [SORT is effective only for ADD and MOVE], default: TOP
field: 'createdAt', // if SORT, this indicates the field to be sorted
},
},
switchVars: {
...otherVars, // switchVars cannot be nested objects
},
mutationResult, // mandatory
ID: '_id', // Set the id field returned by your queries, default: id
}
For maximum flexibility you can also override the default actions of ADD
and REMOVE
operations.
Add 1 to all queries which data type is a number:
operation: {
type: 'ADD',
add: ({ query, type, data, variables }) => {
if (type === 'number') {
return data + 1;
}
}
}
Pass a custom action for the query articles
operation: {
type: 'ADD',
add: ({ query, type, data, variables }) => {
if (query === 'articles') {
return [mutationResult, ...data]; // if you have mixed queries you need to extend the mutationResults with the missing local fields here too
}
}
}
When it's an array of objects mutationResult
must contain the right __typename
field too.
Use the custom add/remove if you want to:
- override the default behavior for arrays
- override the default behavior for numbers
- add a custom function to handle strings (not handled by default)
- affect other variables depending on the query data
- you have specific needs that default actions do not satisfy
Note:
- when using custom
add
and/orremove
sorting is disabled and will be ignored even if you set it. It's up to you to do the sorting in the custom function - if you do not return the mutated data (or it is undefined) the custom add/remove function's result will be skipped and default actions'result will be used instead. However the logic inside the custom function will always be executed.
- if the operation type is MOVE you need to pass both custom
add
andremove
. Passing just one of them will not work.
There are edge situations where the cache includes queries like:
- articles({})
- articles({"sort":null,"limit":null,"start":null,"where":null})
This typically happen when a query with params like
query articles($sort: String, $limit: Int, $start: Int, $where: JSON) {
articles(sort: $sort, limit: $limit, start: $start, where: $where) {
_id
title
published
flagged
}
}
gets called with either no variables object at all (variables object is not present) or a variables empty object has been passed, such as variables: {}
. This may happen when variables are built programmatically.
articles({})
and articles({"sort":null,"limit":null,"start":null,"where":null})
are not handled by default and will be skipped, that is they will not be affected by the update.
However EDGE cases can be handled passing one of the EDGE searchOperator(s) such as AND_EDGE
and OR_EDGE
.
As an example using searchOperator: 'AND_EDGE' the end result would be:
ADD(to published) | REMOVE(from published) | MOVE(from published to flagged) | |
---|---|---|---|
articles({"published":true}) | +1 | -1 | -1 |
articles({"flagged":true}) | 0 | 0 | +1 |
articles({}) | +1 | -1 | +1/-1 (=no-change) |
articles({"sort":null,"limit":null,"start":null,"where":null}) | +1 | -1 | +1/-1 (=no-change) |
On the other hand queries with no variables included like:
query articles {
articles {
_id
title
published
flagged
}
}
are not considered EDGE cases. If included in the queriesToUpdate
array they will be always updated like the following despite searchOperator that is used:
ADD(to published) | REMOVE(from published) | MOVE(from published to flagged) | |
---|---|---|---|
articles({"published":true}) | +1 | -1 | -1 |
articles({"flagged":true}) | 0 | 0 | +1 |
articles | +1 | -1 | +1/-1 (=no-change) |
In the unlikely case that queriesToUpdate
contains exclusively queries with no paramaters, the searchVariables
should be an emptyObject:
update: (proxy, { data: { createArticle = {} } }) => { // your mutation response
const mutationResult = createArticle;
const updates = ApolloCacheUpdater({
proxy,
queriesToUpdate: [getArticlesNoParams, articlesCountNoParams],
searchVariables: {},
mutationResult,
})
if (updates) console.log(`Article added`)
},
MIT © ric0