尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

用自定义 Hook 封装数据请求:统一 loading、error 与竞态处理

用自定义 Hook 封装数据请求:统一 loading、error 与竞态处理
📅 发布时间:2026/7/15 12:32:21

用自定义 Hook 封装数据请求:统一 loading、error 与竞态处理

在 React 组件里手写fetch+useState+useEffect,第一次写起来很爽,写到第五个组件你就会发现:每个组件里都躺着一坨一模一样的loading、error、data三件套,而且十有八九都藏着一个竞态 bug。这篇就把这坨样板抽成一个可复用的 Hook,顺手把竞态问题一次性解决掉。

先看朴素写法,它到底哪不对

一个典型的「拉用户详情」组件,大多数人第一版会这么写:

function UserProfile({ userId }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { setLoading(true); fetch(`/api/users/${userId}`) .then((res) => res.json()) .then((json) => { setData(json); setLoading(false); }) .catch((err) => { setError(err); setLoading(false); }); }, [userId]); if (loading) return <p>加载中…</p>; if (error) return <p>出错了:{error.message}</p>; return <div>{data?.name}</div>; }

这段代码有三个问题,前两个是「烦」,第三个是「错」:

  1. 样板重复:换个接口,这十几行几乎原样再抄一遍。
  2. error 不会重置:userId变了重新请求,如果第一次报过错,error还留着旧值,新请求成功了页面可能还在显示错误。
  3. 竞态(race condition):userId从 1 快速切到 2,两个请求同时在飞。如果 1 的响应比 2 晚回来,setData会被 1 的结果覆盖——你点的是用户 2,页面最后显示的是用户 1。这个 bug 在本地网速快时几乎复现不出来,一上线就有人报。

把它抽成一个 Hook

先把三件套和「请求成功/失败」的状态流转收进一个 Hook。这里用AbortController来处理竞态——组件卸载或依赖变化时,直接把上一个请求取消掉。

import { useState, useEffect } from "react"; function useFetch(url) { const [state, setState] = useState({ data: null, loading: true, error: null, }); useEffect(() => { // 每次 url 变化都新建一个控制器,旧请求会被 abort const controller = new AbortController(); // 重新进入请求态,顺手清掉上一轮的 error setState({ data: null, loading: true, error: null }); fetch(url, { signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then((data) => setState({ data, loading: false, error: null })) .catch((error) => { // abort 触发的报错不是真错误,直接忽略,别写进 state if (error.name === "AbortError") return; setState({ data: null, loading: false, error }); }); // 清理函数:url 变化或卸载时取消在途请求 return () => controller.abort(); }, [url]); return state; }

组件侧一下清爽下来:

function UserProfile({ userId }) { const { data, loading, error } = useFetch(`/api/users/${userId}`); if (loading) return <p>加载中…</p>; if (error) return <p>出错了:{error.message}</p>; return <div>{data.name}</div>; }

竞态为什么就没了?因为userId一变,useEffect的清理函数先跑controller.abort(),把用户 1 的在途请求取消,它的.then就走进AbortError分支被忽略,永远不会setData。最终落地的一定是最后一次请求的结果。

坑一:把三个 state 合并成一个对象,不要拆成三个

你可能想把data / loading / error拆成三个useState。别这么干。拆开后你在 catch 里要连写setData(null); setLoading(false); setError(err),三次 setState 触发的中间渲染可能出现「loading 已 false 但 error 还没更新」的空窗态。合并成一个对象、一次setState落地,状态永远自洽。

坑二:URL 拼接对象参数导致死循环

useFetch依赖url这个字符串,字符串按值比较,稳。但如果你想扩展成useFetch(url, options),而options是组件里每次渲染新建的对象字面量:

// 反例:每次渲染 options 都是新对象,useEffect 每次都重跑,死循环 const { data } = useFetch("/api/list", { headers: { "X-Token": token } });

对象每次渲染引用都变,useEffect的依赖比较认为「变了」,于是无限请求。解决办法是把 options 用useMemo固定引用,或者干脆把它序列化后作为依赖:

function useFetch(url, options) { // 用序列化后的字符串做依赖,内容不变就不重跑 const optionsKey = JSON.stringify(options ?? {}); useEffect(() => { const controller = new AbortController(); setState({ data: null, loading: true, error: null }); fetch(url, { ...options, signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then((data) => setState({ data, loading: false, error: null })) .catch((error) => { if (error.name === "AbortError") return; setState({ data: null, loading: false, error }); }); return () => controller.abort(); // 依赖用 optionsKey 而不是 options 本身,避开引用陷阱 // eslint-disable-next-line react-hooks/exhaustive-deps }, [url, optionsKey]); return state; }

坑三:需要「手动重新请求」怎么办

上面的 Hook 只在url变化时请求。但很多场景要一个「重试」按钮,或者提交表单后主动刷新列表。加一个refetch,用一个自增的 tick 触发useEffect:

import { useState, useEffect, useCallback } from "react"; function useFetch(url) { const [state, setState] = useState({ data: null, loading: true, error: null }); const [tick, setTick] = useState(0); // refetch 只是把 tick +1,间接触发下面的 effect,引用保持稳定 const refetch = useCallback(() => setTick((t) => t + 1), []); useEffect(() => { const controller = new AbortController(); setState({ data: null, loading: true, error: null }); fetch(url, { signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then((data) => setState({ data, loading: false, error: null })) .catch((error) => { if (error.name === "AbortError") return; setState({ data: null, loading: false, error }); }); return () => controller.abort(); }, [url, tick]); // tick 变化即重新请求 return { ...state, refetch }; }

用起来:

function UserProfile({ userId }) { const { data, loading, error, refetch } = useFetch(`/api/users/${userId}`); if (loading) return <p>加载中…</p>; if (error) return ( <div> <p>出错了:{error.message}</p> <button onClick={refetch}>重试</button> </div> ); return <div>{data.name}</div>; }

什么时候别自己写,直接上库

这个 Hook 适合中小项目、或你想搞清楚原理时用。但如果你的应用有大量数据请求,还需要缓存、请求去重、后台自动刷新、分页,自己维护这些会越来越重——这时候直接上 TanStack Query(React Query)或 SWR,它们把缓存和竞态都处理好了,自己造轮子不划算。判断标准很简单:只是想去掉样板 + 修竞态,自定义 Hook 够了;开始需要缓存和跨组件共享数据,换库。

小结

  • 手写fetch + useState + useEffect的三件套样板,重复且容易藏竞态 bug,抽成useFetch一次解决。
  • 把data / loading / error合并成一个对象、一次setState落地,状态自洽,不会出现中间空窗态。
  • 竞态用AbortController治:依赖变化时清理函数abort()掉在途请求,AbortError分支直接忽略,最终落地的永远是最后一次请求。
  • 三个高频坑:对象型 options 用JSON.stringify做依赖避免死循环;要手动刷新用自增tick+useCallback稳定refetch引用。
  • 一句话记忆点:自定义 Hook 治样板和竞态,缓存和共享交给 React Query。

相关新闻

  • 软件授权与机器码绑定:从离线验证到在线激活的演进之路
  • 终极指南:如何配置Autopage自动翻页脚本实现无缝浏览体验
  • C/C++逆向分析:多编译配置下main函数定位策略与实战

最新新闻

  • 上海浪琴中国官方售后服务体系全攻略|官方网站权威公告(2026年7月最新) - 浪琴中国服务中心
  • Navicat 16/17 Mac版无限试用期重置终极指南:三种方法实现永久免费使用
  • Sketch Measure插件:UI设计师的终极标注与规范导出完整指南
  • ChatGPT停止序列设置终极指南:2024年Q2最新API行为变更预警(stop_sequences字段已悄然支持正则匹配雏形)
  • 2026 年杜集废铜回收 电缆电线回收 工业设备回收商家实测排名 - LYL仔仔
  • Windows更新卡住了?5分钟快速修复终极指南

日新闻

  • 告别启动盘残留:用Diskpart彻底清除U盘EFI分区与恢复完整空间
  • 2026 年宜春诚信的塑料缠绕膜厂家哪个好,缠绕膜背后的秘密:你不知道的成本陷阱 - 领域鉴赏官
  • Arch ECS 入门指南:10分钟掌握C#高性能数据驱动架构

周新闻

  • IX9104 PCIe5.0 高速交换芯片@ACP#完整规格 + 应用场景总结
  • Unity游戏集成Coze智能体:实现NPC智能对话与知识库联动
  • SAP EPIC 建行回单查询:从标准类CL_EPIC_EXAMPLE_CN_CCB_GHTD到Z类的5处关键修改

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号