如何使用Apollo Client 3与React实现数据实时展示及跨页同步
问题
- 如何无需刷新网页即可实时展示数据库中的数据
- 如何使用Apollo Client 3完成数据更新操作,实现新增分类后自动同步展示到所有其他页面
现有实现代码
全量分类拉取逻辑
async componentDidMount() { setInterval(async()=>{ await client.query({ query: LISTER_CATEGORIE }, ) .then((response) => { this.setState({ ...this.state, categories: response.data.listerCategorie }); }) .catch((err) => console.error(err)); },3000) }
Apollo Client实例配置
export const client = new ApolloClient({ link: ApolloLink.from([ onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) graphQLErrors.map(({ message, locations, path }) => console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)); if (networkError) console.log(`[Network error]: ${networkError}`);}), new createUploadLink({ uri: 'http://localhost:8080/graphql'}) ]), cache: new InMemoryCache({ typePolicies: { Query: { fields: { categorie(existingData, { args, toReference }) { return existingData || toReference({ __typename: 'Categorie', id: args.id }); }, // 其余配置省略
新增分类实现
async ajouterCategorie() { await client.mutate({ mutation: AJOUTER_CATEGORIE, variables: { titre: this.state.newCategorie.titre, presentation: this.state.newCategorie.presentation, image: '', dateCreation: new Date(), file: this.state.file, cours: [] }, refetchQueries: [ { query: LISTER_CATEGORIE } ], }) .then((response) => { this.props.add(response.data.ajouterCategorie) this.pushNotify("Catégorie Ajouté avec succès", 'success', "Ajout Categorie") }) .catch((err) => console.error(err)); this.handleClose() }
问题原因
- 手写
setInterval轮询不符合Apollo Client设计逻辑,裸调用client.query默认优先读缓存,即使定时触发也大概率拿到旧缓存值,且没有做组件卸载时的定时器清理,存在内存泄漏。 refetchQueries只会触发指定查询的重新拉取,但其他页面如果用一次性的client.query拿数据,没有订阅查询结果变化,不会收到新数据触发重渲染。- InMemoryCache没有配置分类列表字段的合并规则,新增数据无法自动归一化写入列表缓存,导致跨组件读缓存时拿不到最新值。
- 没有在新增分类的mutation回调里主动更新全局缓存,完全依赖refetch的网络请求,延迟高且不可靠。
解决方案
1. 替换手写轮询,用Apollo原生查询订阅能力
删掉手写的setInterval,用watchQuery发起查询,它会自动订阅缓存变化,内置轮询能力,且能在组件卸载时手动取消订阅避免内存泄漏:
async componentDidMount() { // 发起可观察查询,自动订阅缓存更新 this.categoryQuery = client.watchQuery({ query: LISTER_CATEGORIE, fetchPolicy: 'cache-and-network', // 先返回缓存值快速渲染,再拉网络数据更新 pollInterval: 3000, // 需要轮询就开这个配置,比手写setInterval稳定 }); // 订阅数据变化,更新组件状态 this.querySubscriber = this.categoryQuery.subscribe({ next: (response) => { this.setState({ categories: response.data.listerCategorie }); }, error: (err) => console.error(err) }) } // 组件卸载时必须取消订阅,清轮询 componentWillUnmount() { this.querySubscriber?.unsubscribe(); }
生产环境更推荐用GraphQL Subscription + WebSocket实现真正的实时推送,数据库数据变更时由服务端主动推送给所有客户端,没有轮询延迟,资源占用更低,需要后端配合支持WebSocket传输和订阅解析器。
2. 主动更新缓存,实现全页面数据同步
不要只依赖refetchQueries,在mutation的update回调里直接写入新数据到全局缓存,所有订阅该缓存的组件(包括其他页面)会自动触发重渲染,不需要等网络请求返回:
await client.mutate({ mutation: AJOUTER_CATEGORIE, variables: { titre: this.state.newCategorie.titre, presentation: this.state.newCategorie.presentation, image: '', dateCreation: new Date(), file: this.state.file, cours: [] }, // 核心:手动更新缓存 update(cache, { data: { ajouterCategorie } }) { // 读取当前缓存里的分类列表 const existData = cache.readQuery({ query: LISTER_CATEGORIE }); // 新分类追加到列表,写回缓存 cache.writeQuery({ query: LISTER_CATEGORIE, data: { listerCategorie: [...(existData?.listerCategorie || []), ajouterCategorie] } }) }, // refetchQueries可以保留做兜底,防止手动更新缓存出错 refetchQueries: [{ query: LISTER_CATEGORIE }], })
3. 补全缓存类型策略,保证归一化正常
在InMemoryCache配置里加上分类列表字段的合并规则,避免缓存更新时数据被意外覆盖:
cache: new InMemoryCache({ typePolicies: { Query: { fields: { // 新增分类列表字段的合并策略 listerCategorie: { merge(existing = [], incoming) { // 全量列表场景直接返回新值即可,如果是分页场景需要按分页参数合并 return incoming; } }, categorie(existingData, { args, toReference }) { return existingData || toReference({ __typename: 'Categorie', id: args.id }); }, // 其余原有配置保持不变 } } } })
注意事项
- 所有页面读取分类列表时,统一用
useQuery(函数组件)或watchQuery(类组件),不要用一次性的client.query,前者会自动订阅缓存变化,缓存更新时自动重渲染,后者不会。 - 不要在Redux、Context等其他全局状态里重复存分类列表,直接从Apollo缓存读取即可,多副本存储很容易出现数据不一致问题。
内容的提问来源于stack exchange,提问作者platforme stage
相关产品推荐
相关产品推荐

