Mutation 完整生命周期

TanStack Query useMutation 的完整生命周期:onMutate → mutationFn → onSuccess/onError → onSettled,以及乐观更新、回滚、重试的完整模式。

#type / howto #status / evergreen #tech / dev / frontend #resource / react

[!info] related notes

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);
},
创建于 2026/7/3 更新于 2026/7/15