You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apollo Client中Mutation更新与订阅共存导致节点重复问题

解决Apollo Cache更新与订阅同时启用导致的评论重复问题

你遇到的这个问题其实很典型——当你同时用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 10:24:34