首页 / TanStack 生态入门教程 / useMutation 基础

TanStack 生态入门教程

useMutation 基础

本教程共 38 篇 · 第 10 篇 · 更新于 2026-07-27 · 约 9 分钟阅读

TanStackTanStack 生态入门教程useMutationmutatemutateAsync回调mutation 状态变更

10. useMutation 基础

本节目标:掌握 useMutation 的用法,搞懂 mutate 和 mutateAsync 的区别,学会用生命周期回调处理副作用,学完能处理增删改等各种数据修改场景。

10.1 什么是 Mutation

Query(查询)是「读」数据,Mutation(变更)是「写」数据—新增、修改、删除、任何会产生副作用的操作。

为什么不用 useQuery 做这些?因为写操作和读操作的需求不一样:

  • 写操作通常只执行一次,不需要缓存
  • 写操作关心成功/失败结果,要给用户反馈
  • 写操作成功后往往要刷新相关查询
  • 写操作可能要重试、乐观更新、回滚

useMutation 就是专门处理这些的 hook。

10.2 基本用法

看个新增 todo 的例子:

import { useMutation } from '@tanstack/react-query'

function AddTodo() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then((res) => res.json())
    },
  })

  return (
    <div>
      {mutation.isPending ? (
        '添加中...'
      ) : (
        <>
          {mutation.isError && (
            <div>出错了:{mutation.error.message}</div>
          )}
          {mutation.isSuccess && <div>添加成功!</div>}
          <button
            onClick={() => {
              mutation.mutate({ id: Date.now(), title: '洗衣服' })
            }}
          >
            新增 Todo
          </button>
        </>
      )}
    </div>
  )
}

拆解:

  1. mutationFn — 实际执行写操作的函数,接收参数,返回 Promise
  2. mutation.mutate(data) — 触发变更,传入参数
  3. isPending / isError / isSuccess — 三种状态
  4. error / data — 失败时的错误、成功时的返回值

v5 里 useMutation 也是对象签名:useMutation({ mutationFn, ...options })。老写法 useMutation(fn, options) 已移除。

10.3 Mutation 的四种状态

和 Query 的三种状态不同,Mutation 有四种

状态含义
idle / isIdle还没调用过 mutate,初始状态
pending / isPending正在执行
error / isError出错了
success / isSuccess成功了

多了个 idle 状态。因为 Mutation 不是自动触发的,得用户点按钮才执行。没点之前就是 idle。

const mutation = useMutation({ mutationFn: addTodo })

if (mutation.isIdle) {
  // 还没点过按钮
} else if (mutation.isPending) {
  // 正在提交
} else if (mutation.isError) {
  // 失败了
} else if (mutation.isSuccess) {
  // 成功了
}
Note

isIdle 时没有 error 也没有 dataisPending 时也没有最终结果。只有 isError 才有 error,只有 isSuccess 才有 data

10.4 mutate vs mutateAsync

触发变更有两种方式:

const mutation = useMutation({ mutationFn: addTodo })

// 方式一:mutate,不返回 Promise
mutation.mutate(newTodo, {
  onSuccess: (data) => console.log('成功', data),
  onError: (error) => console.log('失败', error),
})

// 方式二:mutateAsync,返回 Promise
try {
  const data = await mutation.mutateAsync(newTodo)
  console.log('成功', data)
} catch (error) {
  console.log('失败', error)
}

用哪个?

  • 大多数场景用 mutate,配合回调处理结果
  • 需要在 await 之后继续做事(比如串联多个操作)用 mutateAsync
Warning

mutate 不返回 Promise,没法 await。如果你写了 await mutation.mutate(...),拿到的是 undefined 不是结果,这是个隐蔽的 bug。要 await 必须用 mutateAsync

React 16 及更早的注意点

mutate 是异步函数,在 React 16 及更早版本里,不能直接把它当事件回调传,因为 React 16 的事件池机制会清空事件对象:

// React 16 及更早:不行
const mutation = useMutation({
  mutationFn: (event) => {
    event.preventDefault()  // 事件已被池化,这里拿不到
    return fetch('/api', new FormData(event.target))
  },
})
return <form onSubmit={mutation.mutate}>...</form>

// React 16 及更早:包一层才行
const onSubmit = (event) => {
  event.preventDefault()
  mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>

React 17+ 没这个问题,事件不再池化。

10.5 生命周期回调

useMutation 提供四个回调钩子,覆盖变更的全生命周期:

useMutation({
  mutationFn: addTodo,
  onMutate: (variables) => {
    // mutate 调用后、mutationFn 执行前
    // 可以返回一个值,会传给后面的 onError/onSuccess/onSettled
    console.log('即将执行', variables)
    return { rollbackId: 123 }
  },
  onError: (error, variables, onMutateResult) => {
    // 失败了
    console.log('失败,回滚用 id:', onMutateResult.rollbackId)
  },
  onSuccess: (data, variables, onMutateResult) => {
    // 成功了
    console.log('成功', data)
  },
  onSettled: (data, error, variables, onMutateResult) => {
    // 不管成功失败都执行
    console.log('结束')
  },
})

执行顺序是:onMutate -> mutationFn -> onSuccessonError -> onSettled

onMutate 返回的值会作为第三个参数传给后面三个回调。这个机制是乐观更新的基础,下一章会细讲。

回调里返回 Promise

回调里如果返回 Promise,会先等 Promise 完成再执行下一个回调:

useMutation({
  mutationFn: addTodo,
  onSuccess: async () => {
    console.log('第一个')  // 先执行
  },
  onSettled: async () => {
    console.log('第二个')  // 后执行
  },
})

这个特性很有用。比如 onSuccess 里调 invalidateQueriesawait 它,能保证数据刷新完了再执行 onSettled,期间 isPending 保持 true。

mutate 上的额外回调

mutate 调用时也能传回调,和 useMutation 上的回调叠加执行:

useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    console.log('先执行')  // useMutation 上的先跑
  },
})

mutation.mutate(todo, {
  onSuccess: () => {
    console.log('后执行')  // mutate 上的后跑
  },
})

用途:useMutation 上的回调处理通用逻辑(比如失效查询),mutate 上的回调处理组件特定逻辑(比如关闭弹窗、跳转页面)。

Warning

mutate 上的回调有个限制:如果组件在 mutation 完成前卸载了,回调不会执行。useMutation 上的回调不受这个限制。所以关键逻辑放 useMutation 上,UI 反馈放 mutate 上。

连续 mutate 的回调差异

连续多次调 mutate,两类回调的触发次数不同:

useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    // 每次 mutate 成功都会执行
  },
})

const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
  mutate(todo, {
    onSuccess: () => {
      // 只对最后一次 mutate(Todo 3)执行一次
    },
  })
})

useMutation 上的回调每次都触发,mutate 上的回调只触发最后一次。因为每次 mutate 会重新订阅 observer,之前的回调被丢弃了。

10.6 重置状态

有时候想清掉 mutation 的 error 或 data(比如关掉错误提示),用 reset

const mutation = useMutation({ mutationFn: createTodo })

// 点击错误信息时清掉
{mutation.isError && (
  <div onClick={() => mutation.reset()}>
    {mutation.error.message}(点击关闭)
  </div>
)}

reset 把状态恢复成 idle,清掉 error 和 data。

10.7 Mutation 重试

和 Query 不同,Mutation 默认不重试。因为写操作重试可能有副作用(比如重复创建数据)。

需要的话手动开:

const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,  // 失败重试 3 次
})

如果设备离线导致失败,mutation 会暂停,等网络恢复后按顺序重试。这个特性配合持久化能做离线优先的应用,但那是进阶话题了。

10.8 Mutation Scopes:串行执行

默认情况下,多次 mutate并行执行的。但有些场景需要串行(比如转账,不能并发):

const mutation = useMutation({
  mutationFn: addTodo,
  scope: {
    id: 'todo',  // 同 id 的 mutation 串行
  },
})

设了 scope.id 后,同 id 的 mutation 会排队执行。前一个没完成,后一个会进入 isPaused 状态等待。

10.9 小结

Mutation 的核心:

  • mutationFn 执行写操作,mutate / mutateAsync 触发
  • 四种状态:idle、pending、error、success
  • 四个回调:onMutate、onSuccess、onError、onSettled,onMutate 的返回值往后传
  • 默认不重试,需要手动开
  • scope.id 让同 id 的 mutation 串行

下一篇讲查询失效,这是 Mutation 和 Query 联动的关键。