← [ BACK_TO_NOTES ]

Mutation 完整生命周期

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

#howto #evergreen #frontend #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,
  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
mutation.mutate
mutation.mutateAsync
mutation.reset

常见错误

onMutate 忘记 cancelQueries

// ❌ 旧请求可能覆盖乐观数据
onMutate: async (newTodo) => {
  queryClient.setQueryData(['todos'], updateFn);
},

// ✅ 先取消再写
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);
},

// ✅ 保存旧数据 + 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/9/16
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本