Apollo Client 2.0中带节流的双向服务器同步实现咨询
嘿,我来帮你梳理下在Apollo Client 2.0里实现这个带节流的双向服务器同步功能的具体方案,毕竟我之前也在项目里处理过类似的场景,踩过一些坑~
核心思路
Apollo Client 2.0自带了强大的缓存管理能力,完全可以替代原来Redux+Saga的状态管理逻辑。我们可以拆解成两个核心场景来实现:
- 本地即时更新 + 节流同步服务器:编辑时先更新Apollo缓存让UI立刻响应,再用节流函数延迟发送mutation到服务器
- 订阅推送时跳过编辑中项:通过本地状态标记正在编辑的TODO ID,收到订阅更新时判断是否需要同步到UI
1. 本地即时更新 + 节流同步服务器
实现步骤
- 用
writeFragment直接修改Apollo缓存中的TODO数据,触发UI即时更新 - 用节流函数(比如Lodash的
_.throttle)包装GraphQL mutation,避免每次按键都发请求 - 处理同步失败的回滚逻辑,确保本地缓存和服务器数据一致
示例代码
import { useApolloClient, useMutation, useMemo } from '@apollo/client'; import { throttle } from 'lodash'; import { useState } from 'react'; // 定义mutation和fragment const UPDATE_TODO = gql` mutation UpdateTodo($id: ID!, $text: String!) { updateTodo(id: $id, text: $text) { id text } } `; const TODO_FRAGMENT = gql` fragment TodoFragment on Todo { id text } `; function TodoItem({ todo }) { const client = useApolloClient(); const [updateTodoMutation] = useMutation(UPDATE_TODO); const [editingTodoId, setEditingTodoId] = useState(null); // 创建节流后的mutation函数,300ms内只执行最后一次 const throttledUpdateTodo = useMemo(() => throttle(async (id, text) => { try { await updateTodoMutation({ variables: { id, text } }); } catch (err) { // 同步失败:回滚本地缓存,重新从服务器拉取最新数据 const { todo: serverTodo } = await client.query({ query: gql` query GetTodo($id: ID!) { todo(id: $id) { ...TodoFragment } } ${TODO_FRAGMENT} `, variables: { id } }); client.writeFragment({ id: `Todo:${id}`, // Apollo默认缓存ID格式:typename:id fragment: TODO_FRAGMENT, data: serverTodo }); } }, 300), [updateTodoMutation, client]); const handleTextChange = (e) => { const newText = e.target.value; // 第一步:更新本地缓存,UI立刻刷新 client.writeFragment({ id: `Todo:${todo.id}`, fragment: TODO_FRAGMENT, data: { ...todo, text: newText } }); // 第二步:调用节流后的mutation同步服务器 throttledUpdateTodo(todo.id, newText); }; return ( <div> <input type="text" value={todo.text} onChange={handleTextChange} onFocus={() => setEditingTodoId(todo.id)} onBlur={() => { setEditingTodoId(null); // 失去焦点时立刻触发同步,避免用户离开后数据未提交 throttledUpdateTodo.flush(); }} /> </div> ); }
2. 订阅推送时跳过编辑中项
实现步骤
- 在组件(或全局状态)中维护
editingTodoId,标记当前正在编辑的TODO ID - 收到订阅的更新数据后,判断推送的TODO ID是否等于
editingTodoId,若相等则跳过缓存更新
示例代码
import { useSubscription, useApolloClient, useEffect } from '@apollo/client'; import { useState } from 'react'; const TODO_UPDATED = gql` subscription TodoUpdated { todoUpdated { ...TodoFragment } } ${TODO_FRAGMENT} `; function TodoList() { const client = useApolloClient(); const [editingTodoId, setEditingTodoId] = useState(null); const { data } = useSubscription(TODO_UPDATED); useEffect(() => { if (data?.todoUpdated) { const updatedTodo = data.todoUpdated; // 只更新非编辑状态的TODO if (updatedTodo.id !== editingTodoId) { client.writeFragment({ id: `Todo:${updatedTodo.id}`, fragment: TODO_FRAGMENT, data: updatedTodo }); } } }, [data, client, editingTodoId]); // 传递editingTodoId给子组件TodoItem,统一维护状态 return ( <div> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} editingTodoId={editingTodoId} setEditingTodoId={setEditingTodoId} /> ))} </div> ); }
额外注意事项
- 缓存ID规则:Apollo默认用
typename:id作为缓存key,确保你的TODO类型定义中包含id字段,或者自定义dataIdFromObject来生成唯一ID - 节流时间调整:根据业务场景选择合适的节流时长(300-500ms比较通用),平衡请求频率和用户体验
- 订阅范围优化:如果是大型列表,建议使用针对单个TODO的订阅(传递
id参数),而非全局订阅,减少不必要的数据传输
内容的提问来源于stack exchange,提问作者Alexander Kachkaev
相关产品推荐
相关产品推荐

