5.1 Query 读,Mutation 写
useQuery 负责读(幂等、可缓存);useMutation 负责写(创建/更新/删除,有副作用、不缓存)。Mutation 不会自动执行,你调用 mutate() 才触发,且需手动决定"写完后如何同步缓存"。
5.2 核心概念
- mutationFn
- 执行写操作的函数,接收 mutate 传入的变量,返回 Promise。如
(newTodo) => api.post('/todos', newTodo)。 - mutate
- 触发 mutation 的方法,"fire-and-forget"式,不返回 Promise。适合在事件回调里直接调用。可传第二参数覆盖当次的 onSuccess/onError。
- mutateAsync
- 返回 Promise 的版本,可
await并 try/catch。适合需要串联多个异步步骤、或按结果决定后续流程的场景——但要自己处理 reject 否则报未捕获错误。 - onMutate
- mutationFn 执行前触发,是乐观更新的关键钩子:在此立即改缓存、并返回快照 context 供回滚。
- onSuccess / onError / onSettled
- 分别在成功、失败、无论成败结束后触发。通常在 onSuccess 里失效相关查询,在 onError 里回滚,在 onSettled 里做最终失效兜底。
- invalidateQueries
- 把匹配的查询标记为 stale 并触发重取。变更后调用它,让列表/详情自动拉取最新数据——最常用的缓存同步方式。
- 乐观更新 (Optimistic Update)
- 不等服务器响应,先假设成功、立即更新 UI;若请求失败,用 onMutate 保存的快照回滚。让交互如"本地操作"般即时。
5.3 基础用法:创建待办
import { useMutation, useQueryClient } from "@tanstack/react-query";
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (title: string) =>
fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ title }),
}).then(r => r.json()),
onSuccess: () => {
// 变更成功 → 失效列表,自动重取最新
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
return (
<button
onClick={() => mutation.mutate("买牛奶")}
disabled={mutation.isPending}
>
{mutation.isPending ? "提交中…" : "添加"}
</button>
);
}
5.4 mutate vs mutateAsync
// mutate:即发即忘,回调里处理结果
mutation.mutate(data, {
onSuccess: (res) => toast("成功"),
onError: (err) => toast("失败"),
});
// mutateAsync:需要 await 或串联时用(记得 try/catch)
try {
const created = await mutation.mutateAsync(data);
navigate(`/todos/${created.id}`);
} catch (e) {
// 必须捕获,否则未处理的 rejection
}
默认优先 mutate
大多数场景用 mutate + 回调更简洁,且不会有"忘记 catch"的风险。只有当你确实需要 await 结果做下一步(如拿到新 id 跳转)时才用 mutateAsync。
5.5 乐观更新完整模式
四个回调协作:onMutate 抢先改缓存、存快照;onError 回滚;onSettled 兜底失效。
const toggleTodo = useMutation({
mutationFn: (todo: Todo) => updateTodo(todo),
// 1. 请求前:立即更新 UI
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previous = queryClient.getQueryData(["todos"]); // 快照
queryClient.setQueryData<Todo[]>(["todos"], (old) =>
old?.map(t => (t.id === newTodo.id ? newTodo : t))
);
return { previous }; // context 传给后续回调
},
// 2. 失败:用快照回滚
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(["todos"], context?.previous);
},
// 3. 成败都:最终与服务器对齐
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
onMutate 里必须先 cancelQueries
否则一个正在进行的后台 refetch 可能在你乐观更新后返回旧数据,把你刚改的 UI 覆盖回去。cancelQueries 会取消进行中的重取,避免这种竞态。
5.6 两种同步策略对比
| 策略 | 做法 | 体验 | 适用 |
|---|---|---|---|
| 失效重取 | onSuccess 里 invalidateQueries | 有短暂 loading | 大多数场景,简单可靠 |
| 直接写入 | onSuccess 里 setQueryData(响应) | 无需再请求 | 响应已含完整数据 |
| 乐观更新 | onMutate 预改 + onError 回滚 | 即时无延迟 | 点赞、勾选等高频轻操作 |
5.7 mutation 状态字段
isPending:正在执行(旧名 isLoading)。isSuccess / isError:结果状态。data / error:成功数据 / 错误对象。reset():清空 mutation 状态,回到 idle。variables:当次 mutate 传入的变量(乐观 UI 常用来渲染"待提交项")。
5.8 小结与下一步
- useMutation 处理写操作,手动 mutate 触发,不自动缓存。
- mutate 即发即忘 + 回调;mutateAsync 可 await 但需自己 catch。
- 变更后靠 invalidateQueries 同步缓存,最省心。
- 乐观更新四步:cancelQueries → 快照 → setQueryData → 失败回滚。
- 下一章系统讲 查询失效与手动更新——invalidate、setQueryData、prefetch。