Mutation 完整生命周期
TanStack Query useMutation 的完整生命周期:onMutate → mutationFn → onSuccess/onError → onSettled,以及乐观更新、回滚、重试的完整模式。
#type / howto
#status / evergreen
#tech / dev / frontend
#resource / react
[!info] related notes
- 前置: TanStack Query 服务端状态
- 所属 MOC: TanStack Query 知识地图
- 关联: 缓存失效模式
- 关联: 请求取消与 AbortController
Mutation 完整生命周期
生命周期流程
mutation.mutate(data)
↓
① onMutate(data) — 乐观更新,返回 context
↓
② mutationFn(data) — 实际的 API 调用
↓
├─ 成功 → ③ onSuccess(data, variables, context)
└─ 失败 → ④ onError(error, variables, context) — 回滚
↓
⑤ onSettled(data, error, variables, context) — 无论成功失败都执行
各阶段详解
onMutate — 请求发出前
在 mutationFn 执行前调用。典型用途:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 1. 取消在途查询,防止覆盖乐观数据
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 2. 保存旧数据(用于回滚)
const previousTodos = queryClient.getQueryData(['todos']);
// 3. 乐观更新缓存
queryClient.setQueryData(['todos'], (old) =>
old?.map(todo => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
);
// 4. 返回 context,传递给 onError 和 onSettled
return { previousTodos };
},
});
返回值会作为 context 传递给后续阶段。
mutationFn — 实际请求
mutationFn: async (newTodo: UpdateTodoInput) => {
const response = await fetch(`/api/todos/${newTodo.id}`, {
method: 'PATCH',
body: JSON.stringify(newTodo),
});
if (!response.ok) throw new Error('更新失败');
return response.json();
},
onSuccess — 请求成功
onSuccess: (data, variables, context) => {
// data: mutationFn 的返回值
// variables: mutate() 传入的参数
// context: onMutate 的返回值
toast.success('更新成功');
// 方式 1:invalidate 让缓存自动刷新
queryClient.invalidateQueries({ queryKey: ['todos'] });
// 方式 2:直接用服务端返回的数据更新缓存
queryClient.setQueryData(['todo', data.id], data);
},
onError — 请求失败
onError: (error, variables, context) => {
// error: 抛出的错误
// variables: mutate() 传入的参数
// context: onMutate 的返回值
// 回滚乐观更新
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
toast.error(`更新失败: ${error.message}`);
},
onSettled — 无论成功失败
onSettled: (data, error, variables, context) => {
// 通用清理逻辑
// 通常在这里 invalidate,确保最终一致
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
完整的乐观更新模式
const updateTodoMutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) =>
old?.map(t => t.id === newTodo.id ? { ...t, ...newTodo } : t)
);
return { previous };
},
onError: (err, newTodo, context) => {
// 回滚
if (context?.previous) {
queryClient.setQueryData(['todos'], context.previous);
}
toast.error('更新失败');
},
onSettled: () => {
// 最终同步
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
// 使用
<button onClick={() => updateTodoMutation.mutate({ id: 1, title: '新标题' })}>
保存
</button>
删除操作的乐观更新
const deleteTodoMutation = useMutation({
mutationFn: deleteTodo,
onMutate: async (todoId) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData(['todos']);
// 乐观删除
queryClient.setQueryData(['todos'], (old) =>
old?.filter(t => t.id !== todoId)
);
return { previous };
},
onError: (err, todoId, context) => {
// 回滚
if (context?.previous) {
queryClient.setQueryData(['todos'], context.previous);
}
},
onSuccess: (data, todoId) => {
// 删除详情缓存
queryClient.removeQueries({ queryKey: ['todo', todoId] });
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
创建操作
const createTodoMutation = useMutation({
mutationFn: createTodo,
// 创建通常不需要 onMutate 乐观更新
// 因为没有旧数据可以回滚
onSuccess: (newTodo) => {
// 方式 1:invalidate 列表
queryClient.invalidateQueries({ queryKey: ['todos'] });
// 方式 2:直接追加到列表缓存(避免 refetch)
queryClient.setQueryData(['todos'], (old) =>
old ? [...old, newTodo] : [newTodo]
);
// 预填详情缓存
queryClient.setQueryData(['todo', newTodo.id], newTodo);
},
});
带重试的 Mutation
const mutation = useMutation({
mutationFn: updateTodo,
retry: 2, // 失败后重试 2 次
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10_000),
});
Mutation 状态
const mutation = useMutation({ mutationFn: updateTodo });
mutation.isPending // 正在执行
mutation.isSuccess // 成功
mutation.isError // 失败
mutation.isIdle // 空闲(未执行)
mutation.isPaused // 离线排队中
mutation.data // 成功时的返回值
mutation.error // 失败时的错误
mutation.variables // 最近一次 mutate() 传入的参数
mutation.mutate // 触发 mutation
mutation.mutateAsync // 返回 Promise 的版本
mutation.reset // 重置状态到 idle
常见错误
onMutate 忘记 cancelQueries
// ❌ 旧请求可能覆盖乐观数据
onMutate: async (newTodo) => {
queryClient.setQueryData(['todos'], updateFn);
// 如果此时有 /todos 的请求在途,它返回后会覆盖乐观数据
},
// ✅ 先取消再写
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
queryClient.setQueryData(['todos'], updateFn);
},
onSuccess 和 onSettled 都 invalidate
// ❌ 重复 invalidate
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] }); // 第一次
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] }); // 第二次
},
// ✅ 只在 onSettled 里 invalidate(推荐)
onSuccess: () => {
toast.success('成功');
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
忘记回滚
// ❌ 乐观更新了但失败后不回滚
onMutate: async (newTodo) => {
queryClient.setQueryData(['todos'], optimisticUpdate);
// 没有保存 previous,也没有 onError 回滚
},
// 结果:失败后 UI 仍然显示乐观数据
// ✅ 保存旧数据 + onError 回滚
onMutate: async (newTodo) => {
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], optimisticUpdate);
return { previous };
},
onError: (err, vars, context) => {
queryClient.setQueryData(['todos'], context.previous);
},