十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TanStack Query React 乐观更新(Optimistic Updates)实战指南:从 UI 变量到缓存直写的两条完整路径

TanStack Query React 乐观更新(Optimistic Updates)实战指南:从 UI 变量到缓存直写的两条完整路径 TanStack Query React 乐观更新Optimistic Updates实战指南从 UI 变量到缓存直写的两条完整路径【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query乐观更新Optimistic Updates是 TanStack Query 在前端先改本地、再同步服务器的典型交互范式。本指南基于 optimistic-updates.md 官方指南系统讲解 React Query 提供的两条实现路径——通过useMutation返回的variables直接驱动 UI以及通过onMutate直接修改 Query Cache并深入本仓库 react-query 与 query-core 源码与 nextjs-app-optimistic-updates 完整示例说明回调时序、上下文透传与回滚的底层实现。读完你能够独立为列表新增、单项编辑等场景编写先显示、后提交、失败回滚的生产级代码。一、为什么需要乐观更新两条路径总览在网络请求真正完成之前用户的增删改意图往往已经十分明确。与其等待服务器响应才刷新界面不如先把预期的结果渲染出来让界面即时反馈。React Query 的useMutation为这种诉求提供了两种官方方式详见 optimistic-updates.md维度方式一Via the UI方式二Via the cache核心机制读取 mutation 结果里的variables在渲染层临时追加 UI通过onMutate直接改写 Query Cache让所有订阅方自动更新是否接触缓存否与 Query Cache 无直接交互是需要cancelQueries/getQueryData/setQueryData复杂度更少更易推理较高需设计快照与回滚失败处理mutation 完成后临时项自动消失可选借助isError保留并重试从onMutate返回值还原快照rollback适用场景乐观结果只在单一组件/单一位置展示屏幕上有多个位置需要感知同一份更新或希望 UI 与缓存保持一致两种方式没有优劣之分选择依据是乐观结果需要在多少个地方展示。下文先讲简单的 UI 方式再讲能力更强的缓存方式。二、方式一通过variables从 UI 层凭空渲染这是较为简单的变体因为不需要触碰缓存。核心思路useMutation的结果对象会把当前调用mutate时传入的参数保存在variables字段中UI 可以在 mutation 处于pending状态时把它当作一个临时列表项渲染。以新增一条 Todo为例完整代码来自 optimistic-updates.mdconst addTodoMutation useMutation({ mutationFn: (newTodo: string) axios.post(/api/data, { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in pending state until the refetch is finished onSettled: () queryClient.invalidateQueries({ queryKey: [todos] }), }) const { isPending, submittedAt, variables, mutate, isError } addTodoMutation这里有一条容易被忽略的注释与黄金约定在onSettled里要return或awaitinvalidateQueries返回的 Promise。因为invalidateQueries内部会先触发 refetch 再 resolve只有把它返回出去mutation 才会一直停留在pending状态直到 refetch 完成——这样临时项才会在列表数据真正刷新后才消失避免闪烁或提前卸载。随后在渲染查询结果的列表处于 mutationisPending期间向列表追加一项示例代码ul {todoQuery.items.map((todo) ( li key{todo.id}{todo.text}/li ))} {isPending li style{{ opacity: 0.5 }}{variables}/li} /ul关键点variables就是调用mutate(variables)时传入的那份数据在这里是待新增的 todo 文本用opacity: 0.5一类弱化样式明确表达尚未持久化当 mutation 成功且 refetch 完成后该临时项会自动消失此时因为服务器已返回真实数据新条目会以正常样式出现在列表里。仓库中的真实运行示例examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx完整实现了本方案——后端 APIapp/api/todos/route.ts会随机让约 30% 的 POST 请求失败便于直观观察两种方案的行为差异。失败时variables不会被清空可安全保留并重试如果 mutation 出错临时项也会随之消失。但好消息是variables在 mutation 出错时并不会被清空onError之后你仍然能读到它因此我们可以决定失败后继续展示它甚至渲染一个重试按钮示例代码{ isError ( li style{{ color: red }} {variables} button onClick{() mutate(variables)}Retry/button /li ) }isPending与isError都是基于 mutationstatus派生的布尔值。从源码看这些派生逻辑位于MutationObserver的#updateResultmutationObserver.tsisPending对应state.status pendingisError对应state.status error其余isSuccess/isIdle同理最终通过useSyncExternalStore订阅变更useMutation.ts。三、跨组件场景用useMutationStatemutationKey共享乐观状态通过 UI 方式在 mutation 与查询位于同一组件时非常顺手。但实践中触发提交的按钮组件与渲染列表的组件往往并不相邻。此时可以借助专用 HookuseMutationState读取 MutationCache 中的任意 mutation 状态最理想的组合是配合mutationKey使用示例代码// somewhere in your app const { mutate } useMutation({ mutationFn: (newTodo: string) axios.post(/api/data, { text: newTodo }), onSettled: () queryClient.invalidateQueries({ queryKey: [todos] }), mutationKey: [addTodo], }) // access variables somewhere else const variables useMutationStatestring({ filters: { mutationKey: [addTodo], status: pending }, select: (mutation) mutation.state.variables, })需要注意返回值恒为数组。因为同一时间可能有多个匹配的 mutation 并行运行比如用户连续点击添加useMutationState会返回匹配到的全部 mutation所以这里的variables是Array。官方指南optimistic-updates.md提示如果列表项需要唯一key可以改选mutation.state.submittedAtmutation 提交时刻的时间戳。这正是并发乐观更新Concurrent Optimistic Updates得以轻松实现的根基——每个 mutation 都拥有独立的variables与submittedAt可以并行渲染多个 pending 项而互不冲突。底层实现filters 与 select 究竟做了什么从源码看useMutationState的实现非常朴素而清晰useMutationState.ts通过useQueryClient().getMutationCache()拿到全局MutationCache用mutationCache.findAll(options.filters)筛选全部 mutation若传了select就对每个 mutation 调用select(mutation)否则直接返回mutation.state通过mutationCache.subscribe(...)订阅变更并用replaceEqualDeep做深度相等比较避免无意义重渲染。其中筛选参数MutationFilters定义于 utils.ts支持四个字段字段类型含义mutationKeyMutationKey匹配 mutation key前缀匹配exactboolean是否要求 key 完全相等predicate(mutation) boolean自定义谓词函数做任意复杂过滤statusMutationStatus按idle/pending/success/error状态过滤这些 filters 最终统一进入mutationCache.findAllmutationCache.ts经由matchMutation进行过滤。而一次mutate()调用会向 MutationCache 中新增一条 mutation 记录保留时间为gcTime因此useMutationState能在任意组件读取到别人发起的 mutation。另外值得一提useIsMutating本质上就是useMutationState之上的一次封装——它把status固定为pending后取数组长度useMutationState.ts。四、方式二直接修改 Query Cache并实现回滚当乐观状态需要被多个位置感知时直接操作缓存是更彻底的手段。但随之而来一个不可回避的问题乐观更新可能在服务器上失败。多数失败场景下重新触发 refetch 即可把数据拉回真实服务器状态但某些服务器故障会导致 refetch 本身不可用这时就必须回滚我们之前写进缓存的乐观值。为此useMutation的onMutate处理器允许你返回一个值该值稍后会作为最后一个参数分别传给onError与onSettled处理器。绝大多数场景下最合理的返回内容是一个恢复快照所需的上下文即之前被乐观更新的数据快照。4.1 回调约定onMutate返回值如何流转先看清整体协议。以列表新增为例optimistic-updates.mdconst queryClient useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) { // Cancel any outgoing refetches // (so they dont overwrite our optimistic update) await context.client.cancelQueries({ queryKey: [todos] }) // Snapshot the previous value const previousTodos context.client.getQueryData([todos]) // Optimistically update to the new value context.client.setQueryData([todos], (old) [...old, newTodo]) // Return a result with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the result returned from onMutate to roll back onError: (err, newTodo, onMutateResult, context) { context.client.setQueryData([todos], onMutateResult.previousTodos) }, // Always refetch after error or success: onSettled: (data, error, variables, onMutateResult, context) context.client.invalidateQueries({ queryKey: [todos] }), })三步走的经典范式await context.client.cancelQueries(...)先取消一切在途 refetch避免旧数据请求晚到、覆盖掉我们即将写入的乐观值快照旧值用getQueryData把缓存中当前值存下来写入乐观值用setQueryData结合函数式更新器(old) [...old, newTodo]追加新条目返回上下文把快照{ previousTodos }作为onMutate的返回值交出去。此后如果 mutation 失败onError收到第 3 个参数onMutateResult即onMutate的返回值用它恢复缓存onSettled则无论成败都触发invalidateQueries让数据最终对齐服务器。4.2 从源码看context与onMutateResult的真实来源这个示例中的onMutate(newTodo, context)与onError(err, newTodo, onMutateResult, context)各自对应什么可以从 mutationObserver.ts 的mutate流程与回调派发代码确认onMutate的第二个参数是一个MutationFunctionContext上下文对象其中包含client即 QueryClient、meta与mutationKey三个字段——注意这里client是小写属性所以代码里调用的是context.client.cancelQueries(...)。该对象的构造见 mutationObserver.tsonMutate的返回值会被保存进 mutation 状态里的context字段MutationState.context见 mutation.ts并在#notify派发成功/失败事件时以第 3 个参数onMutateResult透传给onSuccess/onError/onSettledmutationObserver.ts。因此回调完整签名v5可以概括为onMutate(variables, mutationContext) - context任意值作为 onMutateResult 向后传递 onError(error, variables, onMutateResult, mutationContext) onSuccess(data, variables, onMutateResult, mutationContext) onSettled(data, error, variables, onMutateResult, mutationContext)4.3 单项编辑按[todos, id]粒度更新如果乐观更新针对的是单条 Todo如编辑标题就按列表 key 条目 id的粒度操作缓存optimistic-updates.mduseMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) { // Cancel any outgoing refetches // (so they dont overwrite our optimistic update) await context.client.cancelQueries({ queryKey: [todos, newTodo.id] }) // Snapshot the previous value const previousTodo context.client.getQueryData([todos, newTodo.id]) // Optimistically update to the new value context.client.setQueryData([todos, newTodo.id], newTodo) // Return a result with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the result we returned above onError: (err, newTodo, onMutateResult, context) { context.client.setQueryData( [todos, onMutateResult.newTodo.id], onMutateResult.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo, error, variables, onMutateResult, context) context.client.invalidateQueries({ queryKey: [todos, newTodo.id] }), })与列表场景的唯一差异在于cancelQueries/getQueryData/setQueryData/invalidateQueries的 query key 都带上了newTodo.id从而把影响范围从整个列表收敛到单条记录。回滚时同样根据返回的快照恢复单条数据。4.4 不想写两个回调用onSettled收敛失败分支onError与onSuccess并非强制同时使用。如果你更喜欢单一收口可以在onSettled中根据error参数自行分支optimistic-updates.mduseMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, onMutateResult, context) { if (error) { // do something } }, })注意此写法需要你自行处理回滚逻辑例如在error存在时用onMutateResult.previousTodo恢复缓存因为onError里的自动恢复动作被合并掉了。五、仓库内可直接运行的两方案对照示例本仓库的 nextjs-app-optimistic-updates 示例Next.js App Router在同一页面通过标签页并排展示了这两种方案非常适合对比学习。项目结构如下examples/react/nextjs-app-optimistic-updates/ ├── app/ │ ├── api/todos/ │ │ ├── data.ts # 内存版 todo 数据 │ │ └── route.ts # API 路由约 30% 概率随机失败 │ ├── get-query-client.ts # 创建跨请求复用的 QueryClient │ ├── layout.tsx │ ├── page.tsx # 服务端预取 todos 并 hydrate │ └── providers.tsx └── components/ ├── ApproachTabs.tsx # 切换两种方案的标签页 ├── TodoListCache.tsx # Approach 2onMutate 写缓存 回滚 └── TodoListUI.tsx # Approach 1variables 驱动 UI关键实现细节值得对照阅读Approach 1UI 方式TodoListUI.tsx 在isPending时渲染一个opacity: 0.5的占位li内容直接来自addTodoMutation.variables失败时仅展示错误信息不做回滚Approach 2缓存方式TodoListCache.tsx 在onMutate中先cancelQueries再以getQueryData快照旧列表、生成带optimistic-${Date.now()}前缀 id 的乐观条目并setQueryData追加onError中根据快照恢复onSettled中始终invalidateQueries。界面按todo.id.startsWith(optimistic-)判断临时条目并弱化显示服务端 page.tsx 会在 SSR 阶段预取 todos再通过HydrationBoundary水合模拟真实的请求-渲染时序。启动方式进入该示例目录后按其 package.json 声明安装依赖并运行 dev 脚本如npm install npm run dev然后反复提交新增 todo观察约 30% 的随机失败如何触发UI 方式临时项消失与缓存方式整单回滚两种截然不同的结果。六、如何选择一个决策框架官方指南optimistic-updates.md给出的判断标准非常务实如果乐观结果只需在一个位置展示——优先选UI 方式variables 直接渲染。理由代码量更少、更易推理而且根本不需要处理回滚。临时项在成功后随 refetch 自然消失失败后也只需决定展示 or 不展示。如果屏幕上有多个位置需要感知同一份更新如顶部统计条 列表 详情面板都依赖同一 query——直接操作缓存会自动让所有订阅该 query 的组件同步更新避免在 N 个组件里各自维护一套临时 UI副本。需要额外留意的边界回滚快照可能被后续 mutation 覆盖onError恢复的是onMutate那一刻的快照如果回滚前又有其他 mutation 合法地改写了缓存粗暴恢复可能引入数据竞争——这正是官方进一步阅读材料中并发乐观更新Concurrent Optimistic Updates主题探讨的场景同时叠加useMutationState的submittedAt选择器可缓解部分竞态onSettled中务必returnrefetch 的 Promise让 loading 状态贯穿到数据真正收敛乐观条目最好自带可识别的临时标记如示例中的optimistic-前缀 id既便于弱化样式也便于失败时精准剔除。七、小结一份可直接落地的检查清单两条路径分别对应不同的数据流哲学落地时建议按下列清单逐项核对UI 方式从useMutation结果读取isPending/variables/isError/mutate在列表末尾渲染占位项并加弱化样式UI 方式失败处理确认variables未清空需要时用mutate(variables)提供重试跨组件共享为 mutation 配置mutationKey用useMutationState({ filters, select })读取注意结果恒为数组列表 key 冲突时改用select: (mutation) mutation.state.submittedAt作为唯一键缓存方式onMutate内严格按cancelQueries → getQueryData 快照 → setQueryData 乐观写入 → return 快照顺序执行缓存方式回滚onError第 3 参数即为onMutate返回值用它setQueryData恢复快照缓存方式收尾onSettled内return invalidateQueries(...)让 pending 状态持续到 refetch 结束修改缓存前后始终使用同一套 query key含 id 粒度cancel/get/set/invalidate四者 key 必须一致深入源码对照可继续阅读Hook 层实现 useMutation.ts 与 useMutationState.ts核心状态机 mutation.ts 与回调派发 mutationObserver.ts以及筛选逻辑 utils.ts 与缓存查询 mutationCache.ts。【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表