React+Apollo Client:如何让refetchQueries仅作用于已挂载组件?
问题描述
在React Web应用中使用Apollo Client + TypeScript Codegen Hooks执行查询和变更操作时,遇到以下问题:如何确保Apollo不会自动重新获取已卸载组件关联的查询?
场景细节:
- 存在
Events组件,通过useListEventsQuery钩子获取事件数据展示,该组件在SPA多个位置渲染,通过不同变量过滤不同类型事件。 - 部分包含
Events组件的页面执行变更操作时会生成新事件,但变更响应不返回这些事件,因此通过refetchQueries将ListEvents查询加入到多个变更中,保证组件数据最新。 - 当前问题:Apollo会重取会话中所有运行过的
ListEvents查询实例,包括已卸载组件绑定的,而我只想重取当前挂载组件对应的查询。cache-and-network策略已经能保证组件重新挂载时获取最新数据。
核心疑问:
- Apollo重取所有
ListEvents查询实例(哪怕仅一个关联挂载组件)是预期行为吗? - 有没有方案让变更仅请求用户当前查看的事件数据?
曾考虑为每个场景创建唯一GraphQL查询,但会增加复杂度且降低复用性(仅变量不同,查询逻辑完全一致)。
参考代码
查询钩子
const { data } = useListEventsQuery({ variables: { filter: { eventTypes: [...] }, sort: [{ field: EventSortableField.CreatedAt, asc: false }], limit: 10, from: addDays(now, -1).toISOString(), }, });
变更操作
const [updateReservation] = useUpdateAssetReservationsMutation({ refetchQueries: [GqlOps.Query.ListEvents] });
解答
1. 这是预期行为吗?
是的。当你在refetchQueries中直接传入查询名称(如GqlOps.Query.ListEvents)时,Apollo Client会查找所有与该查询匹配的缓存条目(不管对应的组件是否挂载)并触发重取。这是因为Apollo默认通过「查询名称+变量」的组合标识缓存条目,仅指定查询名称时,会匹配所有使用该查询的条目。
2. 解决方案
方案一:精准指定重取的查询变量
不要只传查询名称,而是传入包含查询和对应变量的对象,仅重取当前挂载组件对应的查询实例:
const [updateReservation] = useUpdateAssetReservationsMutation({ refetchQueries: [{ query: ListEventsDocument, // 对应useListEventsQuery的底层文档 variables: { filter: { eventTypes: [...] }, // 当前组件使用的变量 sort: [{ field: EventSortableField.CreatedAt, asc: false }], limit: 10, from: addDays(now, -1).toISOString(), }, }] });
这样Apollo只会重取匹配「查询+变量」组合的缓存条目,也就是当前组件对应的查询。
方案二:使用update函数手动更新缓存
如果变更操作能够推断出新事件的结构,推荐用update函数直接修改Apollo缓存,避免触发任何重取请求,性能更优:
const [updateReservation] = useUpdateAssetReservationsMutation({ update(cache, { data }) { // 构造新事件对象(可从变更响应提取或根据业务逻辑生成) const newEvent = { __typename: 'Event', id: 'new-event-id', // 其他事件字段... }; // 读取当前缓存中的ListEvents数据 const existingData = cache.readQuery<ListEventsQuery>({ query: ListEventsDocument, variables: { /* 当前组件的变量 */ }, }); if (existingData?.listEvents) { // 将新事件添加到列表头部 cache.writeQuery({ query: ListEventsDocument, variables: { /* 当前组件的变量 */ }, data: { listEvents: [newEvent, ...existingData.listEvents], }, }); } }, });
这种方式完全不需要重取,直接操作缓存,适合变更后能确定新数据结构的场景。
方案三:使用useApolloClient手动触发指定查询的重取
在组件内部通过useApolloClient获取客户端实例,在变更完成后手动调用refetch方法:
import { useApolloClient } from '@apollo/client'; import { useListEventsQuery } from './generated/graphql'; const Events = () => { const client = useApolloClient(); const { data, variables } = useListEventsQuery({ variables: { /* 当前组件的变量 */ }, }); const [updateReservation] = useUpdateAssetReservationsMutation({ onCompleted: () => { // 仅重取当前组件对应的查询 client.refetchQueries({ include: [{ query: ListEventsDocument, variables, }], }); }, }); // 组件渲染逻辑... };
方案四:为查询添加context标识,过滤重取目标
在查询时添加自定义context,然后在refetchQueries中通过filter函数筛选需要重取的查询:
// Events组件中的查询 const { data } = useListEventsQuery({ variables: { /* 当前组件的变量 */ }, context: { isActiveEventsQuery: true }, // 自定义标识 }); // 变更操作 const [updateReservation] = useUpdateAssetReservationsMutation({ refetchQueries: { include: [ListEventsDocument], filter: ({ context }) => { // 仅重取带有指定context标识的查询(即当前挂载的组件) return context?.isActiveEventsQuery; }, }, });
注意:该方案需要确保只有当前挂载的组件的查询带有该context标识,适合需要全局控制重取规则的场景。
内容的提问来源于stack exchange,提问作者Knut Marius

