5月28日 00:38
React Query 中如何实现乐观更新?它有哪些优缺点?
乐观更新(Optimistic Update)是 React Query 的核心特性之一,它让应用在服务器响应返回之前就更新 UI,用户操作能获得即时反馈,体验更接近原生应用。
乐观更新的工作原理
乐观更新的核心思路是"先斩后奏":用户触发操作时,立刻把预期结果写入缓存更新 UI,同时发起真实请求;如果服务器确认成功,用真实数据替换乐观数据;如果失败,则回滚到操作前的状态。
整个生命周期分为四步:
- onMutate — 取消进行中的查询,保存当前缓存快照,写入乐观数据
- 请求发出 — mutation 函数执行,等待服务器响应
- onError(失败时)— 用快照回滚缓存,恢复 UI
- onSettled(无论成败)— 让相关查询失效,拉取服务器最新数据
基础实现
以更新待办事项为例,完整的乐观更新代码如下:
typescriptconst mutation = useMutation({ mutationFn: updateTodo, onMutate: async (updatedTodo) => { // 1. 取消进行中的查询,防止竞态覆盖 await queryClient.cancelQueries({ queryKey: ['todos'] }); // 2. 保存当前缓存,用于回滚 const previousTodos = queryClient.getQueryData(['todos']); // 3. 乐观写入缓存 queryClient.setQueryData(['todos'], (old: Todo[]) => old.map(todo => todo.id === updatedTodo.id ? { ...todo, ...updatedTodo } : todo ) ); // 4. 返回上下文,onError 中可拿到 return { previousTodos }; }, onError: (_err, _variables, context) => { // 失败时回滚 if (context?.previousTodos) { queryClient.setQueryData(['todos'], context.previousTodos); } }, onSettled: () => { // 最终和服务器同步 queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); // 触发 mutation.mutate({ id: 1, title: '更新后的标题' });
为什么需要 cancelQueries?
这是面试中经常被追问的点。如果不取消正在进行的查询,可能出现这种情况:onMutate 刚把乐观数据写入缓存,但一个正在后台执行的 refetch 随后返回,把乐观数据覆盖掉。cancelQueries 会中止这些进行中的请求,确保乐观更新不会被意外冲掉。
新增数据的乐观更新
上面的例子是更新已有数据,比较简单。新增数据时有一个额外问题:新项没有服务端返回的真实 ID,需要生成临时 ID,成功后再替换。
typescriptonMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: ['todos'] }); const previousTodos = queryClient.getQueryData(['todos']); // 生成临时 ID const tempId = `temp-${Date.now()}`; queryClient.setQueryData(['todos'], (old: Todo[] = []) => [ ...old, { ...newTodo, id: tempId }, ]); return { previousTodos, tempId }; }, onSuccess: (data, _variables, context) => { // 用服务端真实 ID 替换临时 ID queryClient.setQueryData(['todos'], (old: Todo[] = []) => old.map(todo => todo.id === context.tempId ? { ...todo, id: data.id } : todo ) ); },
并发冲突怎么处理?
当多个乐观更新同时发生时,可能出现后一个覆盖前一个的情况。React Query 的推荐做法是依赖 onSettled 中的 invalidateQueries——每次 mutation 结束后都重新拉取最新数据,让 UI 最终收敛到服务器状态。如果对实时性要求更高,可以使用 queryClient.invalidateQueries 的 refetchType: 'all' 选项,确保所有相关查询立即刷新。
优缺点对比
优点:
- 用户体验显著提升,操作即时反馈,无需等待网络往返
- 减少感知延迟,即使在慢网络下 UI 也能快速响应
- 接近原生应用的交互体验
- 不需要手动管理 loading 和临时 UI 状态
缺点:
- 实现复杂度增加,需要正确处理回滚和缓存同步
- 可能出现短暂的 UI 闪烁——用户先看到更新,失败后又回滚
- 并发场景需要额外考虑冲突处理
- 调试难度更高,问题可能出现在乐观写入、回滚或服务器同步任一环节
什么时候该用?
乐观更新最适合简单、可预测的操作:切换开关、编辑文本、点赞收藏。对于涉及复杂校验、金融计算或不可逆操作的场景,应该等待服务器确认后再更新 UI,避免误导用户。
关键在于权衡:用户对即时反馈的期待,和操作失败时回滚带来的困惑,哪个影响更大。