useMutation 基础
本教程共 38 篇 · 第 10 篇 · 更新于 2026-07-27 · 约 9 分钟阅读
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>
)
}
拆解:
mutationFn— 实际执行写操作的函数,接收参数,返回 Promisemutation.mutate(data)— 触发变更,传入参数isPending/isError/isSuccess— 三种状态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也没有data。isPending时也没有最终结果。只有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 -> onSuccess 或 onError -> onSettled。
onMutate 返回的值会作为第三个参数传给后面三个回调。这个机制是乐观更新的基础,下一章会细讲。
回调里返回 Promise
回调里如果返回 Promise,会先等 Promise 完成再执行下一个回调:
useMutation({
mutationFn: addTodo,
onSuccess: async () => {
console.log('第一个') // 先执行
},
onSettled: async () => {
console.log('第二个') // 后执行
},
})
这个特性很有用。比如 onSuccess 里调 invalidateQueries 并 await 它,能保证数据刷新完了再执行 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 联动的关键。