useMutation Hook 使用指南

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

在日常开发中,我们经常需要处理异步操作(如表单提交、数据更新等),这些操作通常涉及以下问题:

  1. 加载状态管理
  2. 错误处理
  3. 成功后的回调
  4. 乐观更新
  5. 状态重置

传统的写法需要我们手动处理这些问题,而 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>
  );
};

主要优势

  1. 代码更简洁

    • 不需要手动管理 loading 和 error 状态
    • 不需要手动处理 try/catch
    • 生命周期钩子清晰明了
  2. 类型安全

    • 完整的 TypeScript 支持
    • 参数和返回值类型推导
    • 错误类型处理
  3. 状态管理自动化

    • 自动处理加载状态
    • 自动处理错误状态
    • 提供状态重置功能

与 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);
  },
});

主要区别

  1. 使用场景

    • useRequest:主要用于数据查询(GET)
    • useMutation:主要用于数据修改(POST/PUT/DELETE)
  2. 功能特点

    • useRequest
      • 自动请求
      • 缓存支持
      • 轮询功能
      • 依赖请求
    • useMutation
      • 手动触发
      • 乐观更新
      • 回滚支持
      • 生命周期完整
  3. 状态管理

    • useRequest:关注数据的实时性和缓存
    • useMutation:关注操作的状态和结果

生命周期

useMutation 提供了完整的生命周期钩子,让你可以在请求的不同阶段执行自定义逻辑:

  1. onMutate: 在发起请求前调用

    • 可以用于准备工作、验证或乐观更新
    • 返回的值如果是Promise.reject会阻塞后续执行,直接到onError
  2. onSuccess: 请求成功后调用

    • 接收服务器返回的数据和原始变量
    • 可用于更新本地状态或显示成功消息
  3. onError: 请求失败时调用

    • 接收错误对象、原始变量
    • 用于错误处理和恢复机制
  4. onSettled: 请求完成后调用(无论成功或失败)

    • 总是在 onSuccess 或 onError 之后执行
    • 用于清理工作或状态重置

为什么回调可以返回 Promise

所有生命周期钩子都支持返回 Promise,这带来几个重要优势:

  1. 流程控制: 允许在一个生命周期完成后再进入下一阶段

    typescript 复制代码
    onMutate: async (variables) => {
      // 等待缓存操作完成后再发送请求
      await queryClient.cancelQueries(['todos']);
      const previousTodos = queryClient.getQueryData(['todos']);
      // 没有返回reject,说明可以行下
    }
  2. 组合多个操作: 可以将多个相关的异步操作组合在一起

    typescript 复制代码
    onSettled: 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]);

收益总结

  1. 开发效率

    • 减少样板代码
    • 统一的错误处理
    • 自动的状态管理
  2. 代码质量

    • 更好的可维护性
    • 更强的类型安全
    • 更清晰的关注点分离
  3. 用户体验

    • 统一的加载状态
    • 更好的错误反馈
    • 乐观更新支持
  4. 团队协作

    • 统一的开发规范
    • 更容易的代码审查
    • 更好的可测试性

注意事项

  1. 不要在渲染函数中创建 mutation 函数
  2. 合理使用乐观更新
  3. 注意处理并发请求
  4. 合理使用类型系统
  5. 遵循错误处理最佳实践
相关推荐
LeeAt1 分钟前
真的!真的就一句话就能明白this指向问题
前端·javascript
阳火锅2 分钟前
都2025年了,来看看前端如何给刘亦菲加个水印吧!
前端·vue.js·面试
hahala233319 分钟前
ESLint 提交前校验技术方案
前端
夕水41 分钟前
ew-vue-component:Vue 3 动态组件渲染解决方案的使用介绍
前端·vue.js
codehub42 分钟前
TypeScript 高频面试题与核心知识总结
typescript
我麻烦大了44 分钟前
实现一个简单的Vue响应式
前端·vue.js
独立开阀者_FwtCoder1 小时前
你用 Cursor 写公司的代码安全吗?
前端·javascript·github
Cacciatore->1 小时前
React 基本介绍与项目创建
前端·react.js·arcgis
摸鱼仙人~1 小时前
React Ref 指南:原理、实现与实践
前端·javascript·react.js
teeeeeeemo1 小时前
回调函数 vs Promise vs async/await区别
开发语言·前端·javascript·笔记