Chapter 05

useMutation

处理增删改写操作,用生命周期回调联动缓存,并实现"UI 先行、失败回滚"的乐观更新。

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 状态字段

5.8 小结与下一步