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. 遵循错误处理最佳实践
相关推荐
孤客网络科技工作室7 分钟前
每天学一个 Linux 命令(7):cd
java·linux·前端
努力的搬砖人.12 分钟前
Vue 2 和 Vue 3 有什么区别
前端·vue.js·经验分享·面试
Json_1817901448030 分钟前
python采集淘宝拍立淘按图搜索API接口,json数据示例参考
服务器·前端·数据库
珹洺1 小时前
Java-servlet(十)使用过滤器,请求调度程序和Servlet线程(附带图谱表格更好对比理解)
java·开发语言·前端·hive·hadoop·servlet·html
熙曦Sakura1 小时前
【C++】map
前端·c++
黑贝是条狗1 小时前
html 列表循环滚动,动态初始化字段数据
前端·javascript·html
萌萌哒草头将军1 小时前
🔥🔥🔥4 月 1 日尤雨溪突然宣布使用 Go 语言重写 Rolldown 和 Oxc!
前端·javascript·vue.js
搬砖的阿wei2 小时前
从零开始学 Flask:构建你的第一个 Web 应用
前端·后端·python·flask
萌萌哒草头将军2 小时前
🏖️ TanStack:一套为现代 Web 开发打造的强大、无头且类型安全的库集合 🔥
前端·javascript·vue.js
来碗螺狮粉2 小时前
CSR mode下基于react+i18next实践国际化多语言解决方案
react.js