Apollo Client中Mutation更新与订阅共存导致节点重复问题
你遇到的这个问题其实很典型——当你同时用Mutation的update手动更新缓存,又开启了订阅监听新评论时,同一条评论会被两次添加到缓存列表里:一次是你自己的mutation成功后手动加的,另一次是订阅收到服务器推送的同一条评论再加一次,这就导致了缓存里出现两个相同id的条目,React渲染时自然就报key重复的警告了。
下面给你两种实用的解决方案,优先推荐第一种:
解决方案一:在订阅的updateQuery中过滤已存在的条目
这是最直接高效的解决办法,核心思路是:每当订阅收到新评论/回复时,先检查这条内容是否已经在缓存的列表里了(比如当前用户刚创建的那条,已经通过mutation的update加进去了),如果已经存在,就不再重复添加;如果是其他用户创建的新内容,再正常添加到列表中。
修改评论订阅的updateQuery逻辑
_subscribeToNewComments = () => { this.props.COMMENTS.subscribeToMore({ variables: { eventId: this.props.eventId, }, document: gql` subscription newPosts($eventId: ID!) { Post( filter: { mutation_in: [CREATED] node: { event: { id: $eventId } } } ) { node { id body createdAt event { id } author { id } } } } `, updateQuery: (previous, { subscriptionData }) => { const newPost = subscriptionData.data.Post.node; // 检查这条评论是否已经在缓存列表中存在 const postExists = previous.allPosts.some(post => post.id === newPost.id); // 如果已经存在,直接返回原数据,不做修改 if (postExists) { return previous; } // 不存在的话,再克隆并添加新评论 let newPosts = _cloneDeep(previous); newPosts.allPosts.unshift(newPost); return newPosts; }, }); };
同样修改回复订阅的updateQuery逻辑
_subscribeToNewReplies = () => { this.props.COMMENT_REPLIES.subscribeToMore({ variables: { eventId: this.props.eventId, }, document: gql` subscription newPostReplys($eventId: ID!) { PostReply( filter: { mutation_in: [CREATED] node: { replyTo: { event: { id: $eventId } } } } ) { node { id replyTo { id } body createdAt author { id } } } } `, updateQuery: (previous, { subscriptionData }) => { const newReply = subscriptionData.data.PostReply.node; // 检查回复是否已存在 const replyExists = previous.allPostReplies.some(reply => reply.id === newReply.id); if (replyExists) { return previous; } let newPostReplies = _cloneDeep(previous); newPostReplies.allPostReplies.unshift(newReply); return newPostReplies; }, }); };
这样修改后,当你自己创建评论时,mutation的update已经把评论加到了缓存,订阅收到同一条评论时会检测到id已存在,就不会重复添加;而其他用户创建的评论,缓存里没有,会正常添加,完美解决重复问题。
解决方案二:移除Mutation的update,依赖Apollo自动缓存归一化
如果你不想手动管理缓存更新,可以移除Mutation里的update函数,让Apollo的自动缓存归一化来处理。Apollo会自动把mutation返回的评论对象存入缓存,不过需要注意:Apollo的自动更新不会自动更新allPosts这类列表查询的结果,所以你需要用refetchQueries来重新获取列表。
比如修改你的CREATE_EVENT_COMMENT调用:
this.props.CREATE_EVENT_COMMENT({ variables: { eventId, body, authorId, }, optimisticResponse: { __typename: 'Mutation', createPost: { __typename: 'Post', id: fakeId, body, createdAt: fakeTime, reply: null, event: { __typename: 'Event', id: eventId, }, author: { __typename: 'User', id: authorId, }, }, }, // 移除原来的update函数,添加refetchQueries重新获取列表 refetchQueries: [{ query: queryToUpdate, variables: { eventId } }] });
不过这种方案的缺点是,refetchQueries会重新发起一次查询,不如第一种方案高效,而且乐观响应的体验可能会有细微差别,所以更推荐第一种方案。
最后再提醒一句:你已经在使用id作为列表项的key了,这是React渲染列表的最佳实践,一定要保持这个习惯,避免不必要的重渲染和警告。
内容的提问来源于stack exchange,提问作者Evanss

