useMutation Hook 使用指南
目录
- [为什么需要 useMutation](#为什么需要 useMutation "#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-usemutation")
- [传统写法 vs useMutation](#传统写法 vs useMutation "#%E4%BC%A0%E7%BB%9F%E5%86%99%E6%B3%95-vs-usemutation")
- [与 useRequest 的对比](#与 useRequest 的对比 "#%E4%B8%8E-userequest-%E7%9A%84%E5%AF%B9%E6%AF%94")
- 生命周期
- 使用场景和示例
- 最佳实践
为什么需要 useMutation
在日常开发中,我们经常需要处理异步操作(如表单提交、数据更新等),这些操作通常涉及以下问题:
- 加载状态管理
- 错误处理
- 成功后的回调
- 乐观更新
- 状态重置
传统的写法需要我们手动处理这些问题,而 useMutation 提供了一个优雅的解决方案。
传统写法 vs useMutation
传统写法
typescript
const CategoryForm = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const handleSubmit = async (values: CategoryFormData) => {
setLoading(true);
setError(null);
try {
const response = await request.post('/api/category', values);
message.success('添加成功');
// 刷新列表
tableRef.current?.reload();
} catch (err) {
setError(err as Error);
message.error('添加失败:' + (err as Error).message);
} finally {
setLoading(false);
}
};
return (
<Form onFinish={handleSubmit}>
<Form.Item>
<Button loading={loading} type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
{error && <div className="error-message">{error.message}</div>}
</Form>
);
};
使用 useMutation
typescript
const CategoryForm = () => {
const { mutate, isLoading, error } = useMutation(
(values: CategoryFormData) => request.post('/api/category', values),
{
onSuccess: () => {
message.success('添加成功');
tableRef.current?.reload();
},
onError: (error) => {
message.error('添加失败:' + error.message);
}
}
);
return (
<Form onFinish={mutate}>
<Form.Item>
<Button loading={isLoading} type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
{error && <div className="error-message">{error.message}</div>}
</Form>
);
};
主要优势
-
代码更简洁
- 不需要手动管理 loading 和 error 状态
- 不需要手动处理 try/catch
- 生命周期钩子清晰明了
-
类型安全
- 完整的 TypeScript 支持
- 参数和返回值类型推导
- 错误类型处理
-
状态管理自动化
- 自动处理加载状态
- 自动处理错误状态
- 提供状态重置功能
与 useRequest 的对比
useRequest
typescript
const { data, loading, run } = useRequest(fetchData, {
manual: true,
onSuccess: (result) => {
console.log(result);
},
});
useMutation
typescript
const { mutate, isLoading, data } = useMutation(updateData, {
onSuccess: (result) => {
console.log(result);
},
});
主要区别
-
使用场景
- useRequest:主要用于数据查询(GET)
- useMutation:主要用于数据修改(POST/PUT/DELETE)
-
功能特点
- useRequest
- 自动请求
- 缓存支持
- 轮询功能
- 依赖请求
- useMutation
- 手动触发
- 乐观更新
- 回滚支持
- 生命周期完整
- useRequest
-
状态管理
- useRequest:关注数据的实时性和缓存
- useMutation:关注操作的状态和结果
生命周期
useMutation 提供了完整的生命周期钩子,让你可以在请求的不同阶段执行自定义逻辑:
-
onMutate: 在发起请求前调用
- 可以用于准备工作、验证或乐观更新
- 返回的值如果是Promise.reject会阻塞后续执行,直接到onError
-
onSuccess: 请求成功后调用
- 接收服务器返回的数据和原始变量
- 可用于更新本地状态或显示成功消息
-
onError: 请求失败时调用
- 接收错误对象、原始变量
- 用于错误处理和恢复机制
-
onSettled: 请求完成后调用(无论成功或失败)
- 总是在 onSuccess 或 onError 之后执行
- 用于清理工作或状态重置
为什么回调可以返回 Promise
所有生命周期钩子都支持返回 Promise,这带来几个重要优势:
-
流程控制: 允许在一个生命周期完成后再进入下一阶段
typescriptonMutate: async (variables) => { // 等待缓存操作完成后再发送请求 await queryClient.cancelQueries(['todos']); const previousTodos = queryClient.getQueryData(['todos']); // 没有返回reject,说明可以行下 }
-
组合多个操作: 可以将多个相关的异步操作组合在一起
typescriptonSettled: async () => { // 确保这些操作按顺序完成 await queryClient.invalidateQueries(['todos']); await resetForm(); await closeModal(); }
这种设计使 useMutation 在处理复杂的数据修改流程时更加灵活和强大,特别是当操作涉及多个步骤或依赖其他异步操作时。
使用场景和示例
1. 表单提交
typescript
const SubmitForm = () => {
const { mutate, isLoading } = useMutation(submitForm, {
onSuccess: (data) => {
message.success('提交成功');
// 可以直接使用返回的数据
console.log(data);
},
onError: (error) => {
// 错误处理更集中
handleError(error);
}
});
return <Form onFinish={mutate}>...</Form>;
};
2. 批量操作
typescript
const BatchOperation = () => {
const { mutate, isLoading } = useMutation(batchDelete, {
// 操作前的准备工作
onMutate: (variables) => {
// 做一些验证操作
},
// 乐观更新
onSuccess: (data, variables) => {
message.success('操作成功');
},
// 失败
onError: (error, variables) => {
//错误处理
}
});
return (
<Button
loading={isLoading}
onClick={() => mutate({ ids: selectedRows })}
>
批量删除
</Button>
);
};
3. 状态更新
typescript
const StatusToggle = () => {
const { mutate } = useMutation(toggleStatus, {
onMutate: (newStatus) => {
// 保存之前的状态
const previousStatus = currentStatus;
// 立即更新 UI
setStatus(newStatus);
},
onError: (error, variables) => {
// 发生错误时回滚
}
});
return (
<Switch
checked={status}
onChange={(checked) => mutate(checked)}
/>
);
};
4. 文件上传
typescript
const FileUpload = () => {
const { mutate, isLoading } = useMutation(uploadFile, {
onMutate: (file) => {
// 上传前的验证
if (file.size > maxSize) {
// 会阻止后续代码执行
return new Promise.reject('文件过大')
}
},
onSuccess: (response) => {
message.success('上传成功');
setFileList(prev => [...prev, response.data]);
}
});
return (
<Upload
customRequest={({ file }) => mutate(file)}
loading={isLoading}
>
<Button>上传文件</Button>
</Upload>
);
};
最佳实践
1. 封装通用操作
typescript
// hooks/useDeleteMutation.ts
export const useDeleteMutation = (options?: MutationOptions) => {
return useMutation(deleteItem, {
onSuccess: () => {
message.success('删除成功');
},
onError: (error) => {
message.error('删除失败:' + error.message);
},
...options
});
};
// 使用
const { mutate } = useDeleteMutation({
onSuccess: () => {
// 额外的成功处理
reload();
}
});
2. 类型定义
typescript
interface Variables {
id: string;
data: Record<string, any>;
}
interface Response {
success: boolean;
data: any;
}
const { mutate } = useMutation<Response, Error, Variables>(
updateData,
{
onSuccess: (response) => {
// response 有完整的类型提示
if (response.success) {
// ...
}
}
}
);
3. 错误处理
typescript
const { mutate } = useMutation(submitData, {
onError: (error) => {
if (error.code === 'VALIDATION_ERROR') {
// 表单验证错误
form.setFields(error.fields);
} else if (error.code === 'NETWORK_ERROR') {
// 网络错误
message.error('网络异常,请重试');
} else {
// 其他错误
message.error('操作失败:' + error.message);
}
}
});
4. 状态重置
typescript
const { mutate, reset, isError } = useMutation(submitForm);
// 在特定情况下重置状态
useEffect(() => {
if (visible) {
reset(); // 弹窗打开时重置状态
}
}, [visible]);
收益总结
-
开发效率
- 减少样板代码
- 统一的错误处理
- 自动的状态管理
-
代码质量
- 更好的可维护性
- 更强的类型安全
- 更清晰的关注点分离
-
用户体验
- 统一的加载状态
- 更好的错误反馈
- 乐观更新支持
-
团队协作
- 统一的开发规范
- 更容易的代码审查
- 更好的可测试性
注意事项
- 不要在渲染函数中创建 mutation 函数
- 合理使用乐观更新
- 注意处理并发请求
- 合理使用类型系统
- 遵循错误处理最佳实践