Angular Query 乐观更新(Optimistic Updates)完整实战:UI 变量渲染与缓存回滚双路径

Angular Query 乐观更新(Optimistic Updates)完整实战:UI 变量渲染与缓存回滚双路径 Angular Query 乐观更新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导读本文围绕 Angular Querytanstack/angular-query-experimental的乐观更新能力展开讲解在 mutation 尚未完成时如何让界面提前呈现预期结果。读完本文你将掌握两种相互补充的实现路径一是通过 mutation 返回的variables直接在组件模板中临时渲染新条目不触碰缓存、无需回滚二是通过onMutate/onError/onSettled直接操作 QueryClient 缓存配合快照实现失败回滚与重取。文章以仓库示例 examples/angular/optimistic-updates 及 inject-mutation.ts、inject-mutation-state.ts 源码为佐证保证每一步都可在真实代码中落地。一、什么是乐观更新为什么需要它在 Angular Query 中mutation 代表服务端副作用增删改它不会像 query 那样自动执行而是由你调用mutate()触发见 inject-mutation.ts 的注释Unlike queries, mutations are not run automatically。乐观更新Optimistic Updates指的是在 mutation 真正完成之前先让 UI 假设它一定会成功并立刻呈现结果。这样用户无需等待网络往返即可看到反馈感知速度大幅提升真正的服务器数据随后通过失效重取refetch落回界面。Angular Query 提供两种乐观更新的实现方式通过 UIvariables不动缓存仅根据 mutation 的当前状态在模板中临时渲染通过缓存onMutate直接改写 QueryClient 缓存让所有订阅该查询的位置同时更新。二、方式一通过 UI 临时渲染不碰缓存这是更简单的变体不直接与缓存交互因此也不需要处理回滚逻辑。核心思路是在 mutation 处于pending状态时把它的入参variables当作临时条目渲染到列表中。2.1 定义 mutation 并保证 pending 状态延续到重取结束下面的injectMutation发起新增 todo 的请求并在onSettled中失效todos查询触发重取addTodo injectMutation(() ({ mutationFn: (newTodo: string) axios.post(/api/data, { text: newTodo }), // 务必 _return_ 失效查询返回的 Promise // 这样 mutation 会一直保持 pending 状态直到重取refetch完成 onSettled: async () { return await queryClient.invalidateQueries({ queryKey: [todos] }) }, }))关键点在于注释强调的行为onSettled要returninvalidateQueries(...)的 Promise。如果忽略返回值mutation 会立即从pending变为settled乐观条目将过早消失出现闪烁。2.2 在模板中用isPending()variables()渲染临时条目由于 Angular Query 的 mutation 结果是一个 signal 代理详见第五节模板中需要用函数调用的方式读取这些信号。在渲染todos查询的列表里当 mutationisPending时追加一个半透明条目Component({ template: for (todo of todos.data(); track todo.id) { li{{ todo.title }}/li } if (addTodo.isPending()) { li styleopacity: 0.5{{ addTodo.variables() }}/li } , }) class TodosComponent {}只要 mutation 处于 pending我们就渲染一个opacity: 0.5的临时条目。当 mutation 完成、失效重取成功之后该条目会自动消失——因为界面上会出现来自服务器的正式条目你几乎不会察觉它曾经只是一个乐观占位。2.3 mutation 失败时variables不会清空可继续展示并重试如果 mutation 出错这个临时条目也会随之消失。但你依然可以通过isError状态把它保留下来因为mutation 失败时variables并不会被清空我们仍能读取它甚至为其提供一个重试按钮Component({ template: if (addTodo.isError()) { li stylecolor: red {{ addTodo.variables() }} button (click)addTodo.mutate(addTodo.variables())Retry/button /li } , }) class TodosComponent {}点击 Retry 再次调用mutate(addTodo.variables())用上一次失败的入参重新发起 mutation。2.4 当 mutation 与 query 不在同一组件injectMutationState通过 UI 变量渲染的路径要求 mutation 和 query 处于同一组件内才能直接访问其状态。如果两者分散在不同组件可以通过专用 APIinjectMutationState读取应用内所有mutation 的状态配合mutationKey过滤效果最佳// 应用内的某处 addTodo injectMutation(() ({ mutationFn: (newTodo: string) axios.post(/api/data, { text: newTodo }), onSettled: () queryClient.invalidateQueries({ queryKey: [todos] }), mutationKey: [addTodo], })) // 其他任何地方访问该 mutation 的 variables mutationState injectMutationStatestring(() ({ filters: { mutationKey: [addTodo], status: pending }, select: (mutation) mutation.state.variables, }))注意返回的mutationState是一个数组Signal of Array因为同一时刻可能有多个同 key 的 mutation 并发执行。如果需要在模板中为每个条目生成唯一的trackkey可以让select改选mutation.state.submittedAt这样甚至可以轻松同时展示多个并发乐观更新。从源码看inject-mutation-state.ts 的getResult正是通过mutationCache.findAll(options.filters)找出所有匹配的 mutation再逐个执行select若未提供select则默认返回mutation.state。同时它订阅了mutationCacheinject-mutation-state.ts任何 mutation 状态变化都会经notifyManager.batchCalls批量通知并用replaceEqualDeep做深度相等比较后才更新信号避免无谓的变更触发与模板重绘。三、方式二直接操作缓存快照 回滚当你在 mutation 执行前就乐观地更新了缓存状态mutation 存在失败的可能。大多数失败场景下直接对乐观更新过的查询触发一次 refetch 就能让数据回归真实服务器状态。但某些情况下 refetch 并不奏效——例如错误代表某种无法重取的服务器问题——这时你就需要选择回滚自己的乐观更新。为此injectMutation的onMutate处理器允许你返回一个值该值稍后会作为最后一个参数同时传给onError与onSettled处理器。多数情况下最有用的做法是返回一个快照如旧数据或回滚函数。注意以下示例中无论是cancelQueries、getQueryData还是setQueryData使用的都是注入的queryClient inject(QueryClient)示例代码里亦通过context.client访问同一实例。3.1 新增 todo 时更新 todo 列表queryClient inject(QueryClient) updateTodo injectMutation(() ({ mutationFn: updateTodo, // 当 mutate 被调用时 onMutate: async (newTodo, context) { // 取消所有进行中的 refetch //避免它们覆盖我们的乐观更新 await context.client.cancelQueries({ queryKey: [todos] }) // 对旧值做快照 const previousTodos context.client.getQueryData([todos]) // 乐观地更新为新值 context.client.setQueryData([todos], (old) [...old, newTodo]) // 返回携带快照的结果对象 return { previousTodos } }, // 如果 mutation 失败 // 使用 onMutate 返回的结果进行回滚 onError: (err, newTodo, onMutateResult, context) { context.client.setQueryData([todos], onMutateResult.previousTodos) }, // 无论成功或失败都重新 refetch onSettled: (data, error, variables, onMutateResult, context) { context.client.invalidateQueries({ queryKey: [todos] }) }, }))这段代码构成了缓存型乐观更新的标准三段式范式onMutate发起时先cancelQueries取消可能正在进行的 refetch否则迟到的响应会覆盖乐观写入→ 用getQueryData快照旧数据 → 用setQueryData直接把新 todo 追加进列表缓存 → 把{ previousTodos }返回出去onError失败时用onMutate返回的快照previousTodos覆盖回缓存完成回滚onSettled收尾时无论成败都失效todos查询让真实服务器数据兜底落地。3.2 更新单条 todo如果目标是更新列表中的某一条记录则只需把查询 key 细化到[todos, newTodo.id]快照与回滚都只针对该条目queryClient inject(QueryClient) updateTodo injectMutation(() ({ mutationFn: updateTodo, // 当 mutate 被调用时 onMutate: async (newTodo, context) { // 取消任何进行中的 refetch //避免覆盖乐观更新 await context.client.cancelQueries({ queryKey: [todos, newTodo.id] }) // 快照旧值 const previousTodo context.client.getQueryData([todos, newTodo.id]) // 乐观更新为新值 context.client.setQueryData([todos, newTodo.id], newTodo) // 返回包含旧值和新值的结果 return { previousTodo, newTodo } }, // 如果失败用上面返回的结果回滚 onError: (err, newTodo, onMutateResult, context) { context.client.setQueryData( [todos, onMutateResult.newTodo.id], onMutateResult.previousTodo, ) }, // 无论成功或失败都重新 refetch onSettled: (newTodo, error, variables, onMutateResult, context) { context.client.invalidateQueries({ queryKey: [todos, newTodo.id] }) }, }))这里setQueryData直接写入具体的newTodo对象回滚时则通过onMutateResult.newTodo.id精确定位被修改的 key 并还原previousTodo。3.3 用onSettled替代独立的onError/onSuccess如果你希望统一收口错误与成功处理可以用单个onSettled取代分开的onError/onSuccess在回调内自行判断error是否存在injectMutation({ mutationFn: updateTodo, // ... onSettled: (newTodo, error, variables, onMutateResult, context) { if (error) { // 出错时做点什么 } }, })onSettled无论成败都会被调用回调参数中第一个参数是新数据、第二个是 error通过判空即可分流处理逻辑。四、两种方式如何取舍When to use what如果乐观结果只需要展示在一处使用variables UI 临时渲染是更省代码、也更易于推理的方案——例如完全不需要处理回滚。如果屏幕上有多处位置都需要感知这次更新列表、计数徽标、详情页同时存在那么直接操作缓存会自动让所有订阅了相关 query key 的位置同步更新你只维护一份数据源。五、源码视角injectMutation与injectMutationState如何工作为了让上述 API 真正可用Angular Query 在 query-core 的MutationObserver之上封装了 Angular 响应式层理解它可以避免在使用时踩坑。5.1injectMutation信号化的 mutation 结果查看 inject-mutation.ts 可以看到injectMutation接收一个返回 mutation options 的函数而非静态对象这样 options 可以被computed()包裹而具备响应性。内部核心步骤包括创建基于 query-core 的MutationObserver并通过computed延迟实例化inject-mutation.tsmutate被封装为调用observer.mutate(...)并.catch(noop)inject-mutation.ts因此模板里(click)addTodo.mutate(...)不会因返回的 Promise 而报未处理异常通过effect订阅 observer状态更新时若isPending则注册 AngularPendingTasks计数器避免 pending 期间被 Angular 判定为无任务而误触发 SSR/测试稳定判断inject-mutation.ts最终结果经signalProxy包装后返回inject-mutation.ts。这解释了为何模板中要写addTodo.isPending()、addTodo.variables()而不是addTodo.isPendingsignal 代理把响应式信号暴露为可调用函数调用即读取当前值且 Angular 变更检测时自动完成依赖追踪。同样的约定也适用于injectQuery返回的.data()。5.2injectMutationState跨组件读取 mutationinject-mutation-state.ts 的实现展示了它为何能跨组件工作它不依赖任何组件树而是直接取 QueryClient 的全局MutationCache用findAll(filters)查询匹配的 mutation再配合select投影通过订阅 mutationCache 变更并经过replaceEqualDeep去重后写入响应式信号因此列表展示型组件可以安全地把返回值用于for循环与track。5.3 示例工程中的完整闭环仓库中提供了可运行的真实示例 examples/angular/optimistic-updates。其中 tasks.service.ts 用mutationOptions集中定义了带mutationKey: [tasks]的addTask()onMutate中先cancelQueries、再用getQueryData快照旧列表、随后setQueryData把新任务追加进缓存最后返回previousTodos作为回滚快照onError中把context即返回的快照写回缓存完成回滚onSettled中总是失效tasks查询从服务器重取真实列表。组件的OptimisticUpdatesComponentoptimistic-updates.component.ts通过injectQuery(() this.#tasksService.allTasks())渲染列表addItem()调用addMutation.mutate({ task, failMutation })。界面还提供了一个Fail Mutation 复选框勾选后请求会指向错误的 URL参见 mock-api.interceptor.ts从而可直观演示乐观条目瞬间出现 → 失败 → 列表回滚到快照 → 再次重取的完整链路。六、实践要点小结确保onSettled返回invalidateQueries的 Promise让 mutation 在 refetch 完成前始终处于pending避免乐观条目闪烁后消失。缓存型更新前务必cancelQueries否则飞行中的 refetch 响应可能覆盖乐观写入。onMutate的返回值就是回滚的保险单——它会被透传到onError/onSettled的最后一个参数务必在失败分支使用。错误发生后variables不会清空可借此渲染失败条目与 Retry 按钮。并发乐观更新injectMutationState返回数组配合mutationKeyselect(m m.state.submittedAt)可稳定生成唯一标识。需要在多处同步更新就改缓存只需单点展示就用 UI 变量复杂度更低。进阶阅读可参考原指南末尾指向的 TkDodo 关于并发乐观更新Concurrent Optimistic Updates in React Query的博客文章其中讨论了多 mutation 并发时更精细的快照合并策略Angular Query 下同一套injectMutation/injectMutationState语义同样适用。【免费下载链接】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),仅供参考