apollo 缓存
by ric0
由ric0
突变后如何更新Apollo客户端的缓存 (How to update the Apollo Client’s cache after a mutation)
Apollo客户端及其缓存 (The Apollo Client and its cache)
The Apollo Client is used to fetch data from any GraphQL server. The client is small, yet flexible with many awesome features of which the most appreciated one might be the automatic cache updates that come with the client.
Apollo客户端用于从任何GraphQL服务器获取数据。 客户端虽小,但具有很多很棒的功能,但灵活性很高,其中最受赞赏的功能可能是客户端随附的自动缓存更新。
Basically, the Apollo Client automatically inspects the queries and mutations’ traffic on your behalf and uses the latest version of the data it sees in a response so that the local cache is always up to date.
基本上,Apollo客户端会自动代表您检查查询和变异的流量,并使用其在响应中看到的最新数据,以便本地缓存始终保持最新状态。
一个简单的更新 (A simple update)
Let’s, for example, have a query that asks for all articles:
例如,让我们查询一个要求所有文章的查询:
query articles { articles { id title published author { name } }}
We get this data back:
我们得到这些数据:
{ data: { articles: [ { id: '6543757061', title: 'Does It Pay to Be a Writer?', published: true, author: { name: 'John Doe', } }, { id: '6543757062', title: 'The Genius of Insomnia', published: true, author: { name: 'Mike Kinski', } } ] }}
Later we modify the title of the article with id “6543757061”:
稍后我们修改ID为“ 6543757061”的文章标题:
// MUTATIONmutation updateArticle($id: ID! $title: String) { updateArticle(id: $id, title: $title) { id title published author { name } }}
// _update-article.js...this.props.mutate({ mutation: UPDATE_ARTICLE, variables: { id: '6543757061', title: 'I am a new title', },});...
Result:
结果:
articles: [ { id: '6543757061', title: 'I am a new title', published: true, author: { name: 'John Doe', } }, { id: '6543757062', title: 'The Genius of Insomnia', published: true, author: { name: 'Mike Kinski', } } ]
After the mutation succeeded, our cache gets updated automatically because of 2 reasons:
成功完成突变后,由于以下两个原因,我们的缓存会自动更新:
- we included the article id in the mutation response 我们在突变响应中包含了商品ID
- we included the title in the response 我们在回应中加入了标题
Indeed if the id
field on both results matches up, then the title
field everywhere in our UI will be updated automatically.
的确,如果两个结果的id
字段匹配,那么我们UI中所有位置的title
字段都会自动更新。
Basically, you should make your mutation results have all of the data necessary to update the queries previously fetched.
基本上,您应该使您的变异结果具有更新先前获取的查询所需的所有数据。
That’s also why is a best practice to use fragments to share fields among all queries and mutations that are related.
这也是为什么最佳做法是使用片段在所有相关的查询和变异之间共享字段。
However, updating the author name would not have the same result as the previous one because we have no id
field within the author
. To make it work both query and mutation should include the author’s id as well:
但是,更新作者名称将不会具有与前一个名称相同的结果,因为我们在author
没有id
字段。 为了使其有效,查询和变异都应包括作者的ID:
idtitlepublishedauthor { id name}
扩展用例 (Extended use cases)
However this above is the only type of scenario where the in-place update is more than enough. Indeed there are many other common situations that the automatic update is not covering such as:
但是,这是就地更新绰绰有余的唯一类型的方案。 实际上,还有许多其他常见情况未涵盖自动更新,例如:
- article creation 文章创作
- article deletion 文章删除
- filtered lists of articles 筛选的文章列表
and so on.
等等。
Generally, any case where you need to update your cache in a way that is dependent on the data currently in your cache.
通常,在任何情况下,您都需要以依赖于当前缓存中数据的方式更新缓存。
Those are cases that can be solved only in 2 ways:
这些情况只能通过两种方式解决:
- refresh the browser** after the mutation :D 突变后刷新浏览器**:D
directly access the local cache using the
update
function that allows you to manually update the cache after a mutation occurs without refetching data使用
update
功能直接访问本地缓存,该update
功能允许您在发生突变后手动更新缓存,而无需重新获取数据
** considering you’re using the cache-first
default fetchPolicy
**考虑您使用的是cache-first
默认fetchPolicy
While refetchQueries would be the third option, update
is the Apollo’s recommended way of updating the cache after a query. It is explained in full here.
虽然refetchQueries将是第三个选项,但update
是Apollo建议的在查询后更新缓存的方式。 在这里完整解释。
使用更新功能 (Use of the update function)
However, because using the update function gives you full control over the cache, allowing you to make changes to your data model in response to a mutation in any way you like, it quickly became complex to manage your own cache.
但是,由于使用更新功能可以完全控制高速缓存,并允许您以自己喜欢的任何方式对数据模型进行更改以响应突变,因此管理自己的高速缓存很快变得很复杂。
The temptation would be to turn off the Apollo cache by default, but that should never be the case.
诱惑是默认情况下关闭Apollo缓存,但绝不应该这样。
Let’s address the most common challenges you may face when you start managing your own cache directly.
让我们解决直接开始直接管理自己的缓存时可能遇到的最常见的挑战。
始终使用try / catch块 (Use always a try/catch block)
Most of the examples you see, also in the official Apollo’s documentation, look like the following:
您在Apollo的官方文档中也看到的大多数示例如下所示:
const query = gql`{ todos { ... } }`export default graphql(gql` mutation ($text: String!) { createTodo(text: $text) { ... } }`, { options: { update: (proxy, { data: { createTodo } }) => { const data = proxy.readQuery({ query }); data.todos.push(createTodo); proxy.writeQuery({ query, data }); }, },})(MyComponent);
That’s cool, but what happens if the query has not yet been fetched, so is not in your cache as you supposed? proxy.readQuery
would throw an error and the application would crash.
太酷了,但是如果尚未提取查询会发生什么,那么您的缓存中是否没有您预期的那样呢? proxy.readQuery
将引发错误,并且应用程序将崩溃。
Being sure that the query is there would be safe only in simple scenarios. You need to use a try/catch block:
仅在简单情况下,确保查询存在才是安全的。 您需要使用try / catch块:
update: (proxy, { data: { createTodo } }) => { try { const data = proxy.readQuery({ query }); data.todos.push(createTodo); proxy.writeQuery({ query, data }); } catch(error) { console.error(error); }},
Otherwise, you should be damn sure that the query would be in the cache already.
否则,您应该确定查询将已经在缓存中。
The message here is that you’re better off not making assumptions at all. As Dan Abramov wrote explained perfectly in his blog post:
这里的信息是最好不要做任何假设。 正如丹·阿布拉莫夫 ( Dan Abramov )在其博客文章中完美解释的那样:
We can’t predict the exact user interactions and their order. At any point in time, our app may be in one of a mind-boggling number of possible states. We do our best to make the result predictable and limited by our design. We don’t want to look at a bug screenshot and wonder “how did that happen.
我们无法预测确切的用户互动及其顺序。 在任何时间点,我们的应用程序都可能处于令人难以置信的众多可能状态之一。 我们尽力使结果可预测并受设计限制。 我们不希望看到一个错误的截图和惊叹“ 这是怎么发生的。
Keep in mind that both proxy.readQuery and proxy.writeQuery may throw errors independently. As an example, you can successfully read a query from the cache while the write operation will throw an error because of a missing field in data, more than often that missing field would be __typename
请记住, proxy.readQuery和proxy.writeQuery都可能独立引发错误。 例如,您可以成功地从缓存中读取查询,而写操作将由于数据中缺少字段而引发错误,而这种丢失的字段通常是__typename
始终定义查询中使用的变量 (Always define the variables used in the query)
Imagine now that we have a mutation that creates a new article that is already marked as published.
现在想象一下,我们有一个变异,可以创建一个新文章,该文章已被标记为已发布。
Generally, simple examples show a single query retrieving all articles than later are filtered on the Client (ex. articles.filter(article => article.published))
通常,简单的示例显示一个查询,检索所有文章,然后再在客户端上对其进行过滤(例如articles.filter(article => article.published))
To illustrate our point, let’s assume instead that we have a query that retrieves from the server only the published articles.
为了说明我们的观点,让我们假设我们有一个查询,该查询仅从服务器检索发布的文章。
At that point, after the new article mutation completed, we need to read/write the cached query using the published: true
variable to match the exact query we need to update in the cache.
到那时,在完成新文章更改之后,我们需要使用published: true
变量读取/写入缓存的查询,以匹配我们需要在缓存中更新的确切查询。
update: (proxy, { data: { createPublishdedArticles } }) => { try { const data = proxy.readQuery({ query, variables: { published: true } }); data.articles.push(createPublishdedArticles); proxy.writeQuery({ query, variables: { published: true }, data }); } catch(error) { console.error(error); }},
That’s it. While this use case is manageable, since we only have one Boolean variable, it becomes quite tricky once you have more complicated use cases, that include multiple queries and variables.
而已。 尽管此用例是可管理的,但由于我们只有一个布尔变量,一旦具有更复杂的用例(包括多个查询和变量),它就会变得非常棘手。
越来越复杂 (Increasing complexity)
So far we covered just basic cases. When developing any app, things get easily more demanding down the road in terms of cache management.
到目前为止,我们仅介绍了基本案例。 在开发任何应用程序时,就缓存管理而言,事情变得越来越容易。
Indeed while using the Apollo Client updating the local cache becomes exponentially complicated when it needs to include multiple variables, include multiple queries or cover scenarios where Apollo’s in-place update may not be sufficient:
确实,在使用Apollo客户端进行更新时,如果本地缓存需要包含多个变量,包含多个查询或涵盖了Apollo的就地更新可能不足的场景,则本地缓存会成倍地复杂化:
- Add/remove to list 添加/删除到列表
- Move from one list to another 从一个列表移到另一个
- Update filtered list 更新过滤列表
and so on.
等等。
变异后更新多个查询 (Updating more than one query after a mutation)
It generally happens that after a mutation we want to update more than just one query. For example, let’s think we are retrieving all articles in the dashboard component, but also published articles and unpublished articles in two other different components.
通常,发生突变后,我们想要更新多个查询。 例如,让我们以为我们正在检索仪表板组件中的所有文章,还可以在另外两个不同的组件中检索已发布的文章和未发布的文章。
Apollo client will not only write each query in the cache but will do it so that the same query with different variables is stored as 2 different entries. For example, these are our two queries:
Apollo客户端不仅会在缓存中写入每个查询,而且还会这样做,以便将具有不同变量的同一查询存储为2个不同的条目。 例如,以下是我们的两个查询:
// query 1query articles { articles { id title published author { name } }}// will be stored as: articles
// query 2query articles($where: JSON) { articles(where: $where) { id title published author { name } }}/* will be stored as:articles({"where":{"published":true,"sort":"asc"})
when the query is invoked with:{ variables: { where: { published: true, sort: "asc" } } }*/
Those are 2 different queries in the Apollo’s cache as one would expect. However, we want to retrieve also all unpublished articles. To do so we need to additionally invoke “query 2” with the variables where: { published: false, sort: 'asc' }
正如人们所期望的那样,这些是阿波罗缓存中的两个不同查询。 但是,我们也想检索所有未发表的文章。 为此,我们需要使用以下变量另外调用“查询2” where: { published: false, sort: 'asc' }
Doing so you end up with 3 entries in the cache:
这样做最终在缓存中包含3个条目:
articlesarticles({"where":{"published":true,"sort":"asc"}})articles({"where":{"published":false,"sort":"asc"}})
Why is this important? If you’re going to add a new article and want to update the local cache after the mutation, you will need to read more than one query and also the same query multiple times (one time per each set of variables). Like this:
为什么这很重要? 如果要添加新文章并希望在突变后更新本地缓存,则将需要读取多个查询和多次相同查询(每组变量一次)。 像这样:
// STEP #1// update 'articles'try { const dataQuery = proxy.readQuery({ query: getArticles });
dataQuery.articles.push(newArticle);
proxy.writeQuery({ query: getArticles, data: dataQuery });}catch(error) { console.error(error);}
// STEP #2// articles({"where":{"published":true,"sort":"asc"}})try { const dataQuery = proxy.readQuery({ query: getArticles, variables: { { where:{ published: true, sort: "asc", }, }, }, });
dataQuery.articles.push(newArticle);
proxy.writeQuery({ query: getArticles, variables: { { where:{ published: true, sort: "asc", }, }, }, data: dataQuery });}catch(error) { console.error(error);}
// STEP #3// articles({"where":{"published":false,"sort":"asc"}})try { const dataQuery = proxy.readQuery({ query: getArticles, variables: { { where:{ published: false, sort: "asc", }, }, }, });
dataQuery.articles.push(newArticle);
proxy.writeQuery({ query: getArticles, variables: { { where:{ published: false, sort: "asc", }, }, }, data: dataQuery });}catch(error) { console.error(error);}
You should already see where this goes and how easily you will need to add more boilerplate for each query/variables combination.
您应该已经知道了它的去向,以及为每个查询/变量组合添加更多样板的难易程度。
变量的顺序和值 (Variables’ order and values)
It is also worth noting that the variables’ order is very important.
还值得注意的是,变量的顺序非常重要。
These two following queries are not considered the same and will be stored separately in the cache:
以下两个查询不相同,它们将分别存储在缓存中:
// Calling a query
export default graphql(gql` query ($width: Int!, $height: Int!) { dimensions(width: $width height: $height) { ... } ... }`, { options: (props) => ({ variables: { width: props.size, height: props.size, }, }),})(MyComponent);
// Calling the same query above, but with a different order of variables fieldsexport default graphql(gql` query ($width: Int!, $height: Int!) { dimensions(width: $width height: $height) { ... } ... }`, { options: (props) => ({ variables: { height: props.size, width: props.size, }, }),})(MyComponent);
This ends up with the same query stored twice in the cache with a different order of variables:
最后,同一查询以不同的变量顺序存储在缓存中两次:
dimensions({"width":600,"height":600})dimensions({"height":600,"width":600})
Invoke again the same query with different props.size and you get an additional entry in the cache:
再次使用不同的props.size调用同一查询,您将在缓存中获得一个附加条目:
dimensions({"width":600,"height":600})dimensions({"height":600,"width":600})dimensions({"height":100,"width":100})
Crazy, huh? You see how this gets easily out of control if approached naively.
疯了吧? 您会发现,如果天真地采取这种措施,将很容易失控。
边缘情况 (Edge cases)
If that was not enough there is even more.
如果那还不够,那就更多了。
When you define a query with variables you generally use them, too.
当使用变量定义查询时,通常也使用它们。
Let’s consider the following example:
让我们考虑以下示例:
query articles($sort: String, $limit: Int) { articles(sort: $sort, limit: $limit) { _id title published flagged } }
You’re probably going to invoke it like this:
您可能会像这样调用它:
export default graphql(gql`${ABOVE_QUERY}`, { options: (props) => ({ variables: { sort: props.sort, limit: props.limit, }, }),})(MyComponent);
But what about if it 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.
但是,如果根本不使用任何变量对象(不存在变量对象)或已传递了变量空对象(例如variables: {}
),则该怎么办? 当以编程方式构建变量时,可能会发生这种情况。
For example:
例如:
export default graphql(gql`${ABOVE_QUERY}`, { options: (props) => ({ variables: props.varObj, // props.varObj might be an empty object }),})(MyComponent);
stores articles({"sort":null,"limit":null})
in the cache;
将articles({"sort":null,"limit":null})
在缓存中;
while:
而:
export default graphql(gql`${ABOVE_QUERY}`)(MyComponent);
stores articles({})
in the cache.
将articles({})
存储在缓存中。
The above edge cases are more the result of unwanted/unexpected behavior than done on purpose. However, it is good to keep in mind how that query will end in the cache and in what form.
上述边缘情况更多是由于不良/意外行为而不是有意的行为导致的。 但是,最好记住该查询将如何在缓存中以什么形式结束。
在缓存的查询之间移动项目 (Moving items between cached queries)
There could also be the case that we want to unpublish an article. That would mean to move it from the published query to the unpublished one.
在某些情况下,我们可能想取消发表文章。 这意味着将其从已发布的查询移至未发布的查询。
Basically, we first need to save the item from the published list, then remove it and finally add the save item to the unpublished list. Let’s see how it can be done:
基本上,我们首先需要从发布列表中保存项目,然后将其删除,最后将保存的项目添加到未发布列表中。 让我们看看如何做到这一点:
const elementToMoveId = '1';let elementToMove;
try { const dataQueryFrom = proxy.readQuery({ query: getArticles, variables: { { where:{ published: true, sort: "asc", }, }, }, }); elementToMove = dataQueryFrom.articles.filter(item => item.id === elementToMoveId)[0]; dataQueryFrom.articles = dataQueryFrom.articles.filter(item => item.id !== elementToMoveId)
proxy.writeQuery({ query: getArticles, variables: { { where:{ published: true, sort: "asc", }, }, }, data: dataQueryFrom });}catch(error) { console.error(error);}
if (elementToMove) { try { const dataQueryTo = proxy.readQuery({ query: getArticles, variables: { { where:{ published: false, sort: "asc", }, }, }, });
dataQueryTo.articles.push(elementToMove);
proxy.writeQuery({ query: getArticles, variables: { { where:{ published: true, sort: "asc", }, }, }, data: dataQueryTo, }); } catch(error) { console.error(error); }}
阿波罗缓存更新器 (Apollo Cache Updater)
As you see, there are a lot of things to wrap up just to handle very common use cases.
如您所见,为了处理非常常见的用例,有很多事情需要解决。
There is a lot of code to be written and it is prone to error.
有很多代码要编写,并且容易出错。
For those reasons, I published the Apollo Cache Updater, an npm package that is a zero-dependencies helper for updating Apollo’s cache after a mutation. It helped me stay sane while handling the cache :)
出于这些原因,我发布了Apollo Cache Updater ,这是一个npm软件包 ,它是零依赖性助手,用于在突变后更新Apollo的缓存。 它帮助我在处理缓存时保持理智:)
It tries to decouple the view from the caching layer by configuring the mutation’s result caching behavior through the Apollo’s update
variable.
它尝试通过Apollo的update
变量配置变异的结果缓存行为,从而将视图与缓存层分离。
The goal is to cover all the above scenarios by just passing a configuration object.
目的是仅通过传递配置对象来涵盖上述所有场景。
What it does, after you probably run multiple queries with different variables, pagination, etc., is to iterate over every object in ROOT_QUERY performing actions on your behalf you defined in the configuration object you passed.
在您可能使用不同的变量,分页等运行多个查询之后,它的作用是迭代ROOT_QUERY中的每个对象,它们代表您在传递的配置对象中定义的操作。
结论 (Conclusions)
Managing the cache is hard, in any language. Apollo Client gives us many advantages though in more complex scenarios it leaves us, developers, to deal with everything by ourselves. Apollo Cache Updater tries to mitigate that pain a little while waiting for an official, easy to use, solution to automatically add/remove entries to cached queries.
用任何语言都很难管理缓存。 尽管在更复杂的场景中,Apollo Client让我们(开发人员)自己处理所有事情,但它给我们带来了很多优势。 Apollo Cache Updater在等待官方的,易于使用的解决方案以自动向缓存的查询中添加/删除条目时尝试减轻这种痛苦。
Get the npm package here.
在此处获取npm软件包。
apollo 缓存