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. 遵循错误处理最佳实践
相关推荐
神仙别闹17 小时前
基于C语言实现B树存储的图书管理系统
c语言·前端·b树
玄魂17 小时前
如何查看、生成 github 开源项目star 图表
前端·开源·echarts
前端一小卒18 小时前
一个看似“送分”的需求为何翻车?——前端状态机实战指南
前端·javascript·面试
syt_101318 小时前
Object.defineProperty和Proxy实现拦截的区别
开发语言·前端·javascript
遝靑18 小时前
Flutter 跨端开发进阶:可复用自定义组件封装与多端适配实战(移动端 + Web + 桌面端)
前端·flutter
cypking18 小时前
Web前端移动端开发常见问题及解决方案(完整版)
前端
老前端的功夫18 小时前
Vue 3 vs Vue 2 深度解析:从架构革新到开发体验全面升级
前端·vue.js·架构
栀秋66619 小时前
深入浅出链表操作:从Dummy节点到快慢指针的实战精要
前端·javascript·算法
狗哥哥19 小时前
Vue 3 动态菜单渲染优化实战:从白屏到“零延迟”体验
前端·vue.js
青青很轻_19 小时前
Vue自定义拖拽指令架构解析:从零到一实现元素自由拖拽
前端·javascript·vue.js