这是我参与8月更文挑战的第1天,活动详情查看: 8月更文挑战
如题,本文想要实现一个完整的,可在大型项目中实际应用的一个
useFetchData Hooks
,网上有很多类似的实现,但是都没有找到最合适自己的(一千个读者就有一千个哈姆雷特,并非人家实现的不好),所以干脆不如自己撸一个,在此记录一下过程,希望能对大家有点帮助。
【实现的功能如下】:
loading
先看demo: awesome-use-fetch-data_Demo
Demo 仓库地址 -> awesome-use-fetch-data_Repo
本文大体上分为如下几个步骤讲解实现过程:
1 - 为什么要封装一个 useFetchData Hooks
2 - 使用 Typescript 封装一个前端超时取消、页面销毁/路由跳转取消、带 loading 的 fetch 库
3 - 结合二者,实现一个完整版的 useFetchData
为什么要封装一个 useFetchData Hooks
首先,就来说说为什么要实现这样一个 Custom Hooks 呢?原因有两点:
第一,如果你项目里大量使用 Hooks 开发代码,就避免不了封装高复用的 Custom Hooks
如果你还是一直在复制粘贴代码,那么看完本篇文章,相信对你还是有些许裨益的。
第二,在业务场景里,基本上每个页面大部分组件都会请求数据接口渲染,所以对请求进行高复用封装能提高开发效率
具体为什么能提高开发效率,我这边简单写了两个伪代码,参考如下:
// page1.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';
export default function Page1() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
// 组件加载完毕,请求数据
useEffect(() => {
setLoading(true);
axios.get('/user/list', { params: { ID: 12345 } })
// 更新数据
.then(function(res) {
setData(res.data);
setLoading(false);
.catch(function(error) {
console.log(error);
setLoading(false);
}, [])
return (
<Table loading={loading} data={data} columns={columns} />
// page2.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';
export default function Page2() {
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
// 组件加载完毕,请求数据
useEffect(() => {
setLoading(true);
axios.post('/article/list', { page: 'Fred', pageSize: 'Flintstone'})
.then(function (res) {
setList(res.data);
setLoading(false);
.catch(function (error) {
console.log(error);
setLoading(false);
}, [])
return (
<Table loading={loading} data={list} columns={columns} />
// page1.jsx
// page1.jsx
import { useState, useEffect } from 'react';
import useFetchData from 'use-fetch-data';
export default function Page1() {
const [data, setData] = useState([]);
const options = { params: { ID: 12345 } };
const { data, loading } = useFetchData('/user/list', options);
return (
<Table loading={loading} data={data} columns={columns} />
// page2.jsx
import { useState, useEffect } from 'react';
import useFetchData from 'use-fetch-data';
export default function Page2() {
const options = { method: 'POST', data: { page: 'Fred', pageSize: 'Flintstone'}};
const { data, loading } = useFetchData('/article/list', options);
return (
<Table data={data} columns={columns} />
从上面两段代码,可以非常清晰的看出来封装前后的代码对比,封装前,每个业务组件在内部处理请求的同时,还要处理数据和 loading 状态,那么一个组件两个组件还好,当项目庞大起来,几十个接口几百个组件都进行请求,那么项目就有了大量的 CV 操作冗余代码。
由此可见一个 useFetchData Hooks 可以大大精简我们的业务代码,合理的将一个常用的组件或者方法抽象成 Custom Hooks 真的是一个非常好的选择,希望大家也能在平时自己用起来,构建自己的 react-use 之类的 Hooks Utils。
介绍完封装一个 useFetchData Hooks 的充分理由,接下来就是正式进行代码封装了,既然是封装请求库,那么就必须选择一个请求库,我这里以个人比较喜欢的 fetch 为例,此 hooks 核心其实不是请求库,大家只要选择自己擅长的请求库就可以了。
基于 Typescript 封装一个多功能 fetch
上面提到了本文核心其实也不在封装 fetch 这里,因为 fetch 这个库并不是很多人喜欢用,可能大家很多人喜欢用 axios,也没关系,反正做的事情都是差不多的,到时候各位进行自己请求库的替换就可以了,我只是比较喜欢 fetch 而已,ts-fetch 实现了如下功能:
1 - 内部处理异常(需要和后端约定好返回)
2 - 前端自超时,当请求响应时间超过一定阈值,前端认为超时(可以通过传参覆盖)
3 - 利用 AbortController 取消请求(页面销毁/路由跳转)
TS-Fetch 代码地址 -> useful-kit,个人 TypeScript 用的一般,如果有大佬们有更好的封装方法,可以留言或者仓库直接共建,非常感谢。
下面就直接贴封装的请求库代码,写文章,就只是简单的进行封装一下,大家在业务里还可以根据业务特性,再次进行改造到适合自己项目的程度,比如路径参数 api 这种也是可以支持的:
// 这个是同构 fetch,既能在服务端,又能在客户端
import fetch from 'isomorphic-unfetch';
// query 格式化的插件,其实可以自己实现
import qs from 'query-string';
// 捕获异常内部处理的一个提示,和你项目用的 ui 库一致就可以
import { message } from 'antd';
function filterObject(o: Record<string, string>, filter: Function) {
const res: Record<string, string> = {};
Object.keys(o).forEach(k => {
if (filter(o[k], k)) {
res[k] = o[k];
return res;
export enum EHttpMethods {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE'
type ICustomRequestError = {
status: number;
statusText: string;
url: string;
function dealErrToast(err: Error & ICustomRequestError, abortController?: AbortController) {
switch(err.status) {
case 408: {
abortController && abortController.abort();
(typeof window !== 'undefined') && message.error(err.statusText);
break;
default: {
console.log(err);
break;
* @description: 声明请求头header的类型
interface IHeaderConfig {
Accept?: string;
'Content-Type': string;
[propName: string]: any;
export interface IResponseData {
code: number;
data: any;
message: string;
interface IAnyMap {
[propName: string]: any;
export interface IRequestOptions {
headers?: IHeaderConfig;
signal?: AbortSignal;
method?: EHttpMethods;
query?: IAnyMap;
params?: IAnyMap;
data?: IAnyMap;
body?: string;
timeout?: number;
credentials?: 'include' | 'same-origin';
mode?: 'cors' | 'same-origin';
cache?: 'no-cache' | 'default' | 'force-cache';
* Http request
* @param url request URL
* @param options request options
interface IHttpInterface {
request<T = IResponseData>(url: string, options?: IRequestOptions): Promise<T>;
const CAN_SEND_METHOD = ['POST', 'PUT', 'PATCH', 'DELETE'];
class Http implements IHttpInterface {
public async request<T>(url: string, options?: IRequestOptions, abortController?: AbortController): Promise<T> {
const opts: IRequestOptions = Object.assign({
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json'
credentials: 'include',
timeout: 10000,
mode: 'cors',
cache: 'no-cache'
}, options);
abortController && (opts.signal = abortController.signal);
if (opts && opts.query) {
url += `${url.includes('?') ? '&' : '?'}${qs.stringify(
filterObject(opts.query, Boolean),
)}`;
const canSend = opts && opts.method && CAN_SEND_METHOD.includes(opts.method);
if
(canSend && opts.data) {
opts.body = JSON.stringify(filterObject(opts.data, Boolean));
opts.headers && Reflect.set(opts.headers, 'Content-Type', 'application/json');
console.log('Request Opts: ', opts);
try {
const res = await Promise.race([
fetch(url, opts),
new Promise<any>((_, reject) => {
setTimeout(() => {
return reject({ status: 408, statusText: '请求超时,请稍后重试', url });
}, opts.timeout);
const result = await res.json();
return result;
} catch (e) {
dealErrToast(e, abortController);
return e;
const { request } = new Http();
export { request as default };
高阶完整版的 useFetchData
上面已经封装完了一个基于 fetch 的请求库,接下来就是利用它写一个 useFetchData Hooks。这里思考如下:
1 - 使用起来要简单,统一,尽可能的减少请求库逻辑代码在业务组件内
2 - 在 hooks 内部进行错误处理,组件业务层级无需关心处理
3 - 返回响应数据、异常错误和 loading
有个上面三点目标,接下来就是实现了,具体代码如下:
* /hooks/useFetchData.tsx
import { useState, useEffect, useRef } from 'react';
import request, { IRequestOptions, IResponseData } from '../utils/request';
interface IFetchResData {
data: T | undefined;
loading: boolean;
error: any;
function useFetchData<T = any>(url: string, options?: IRequestOptions): IFetchResData {
// 如果是一个通用的 fetchData,那么使用any是没办法的,如果只是针对list,any可以替换为对应的数据范型
const [data, setData] = useState<T>();
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(null);
* 超时或者页面销毁/路由跳转,取消请求
const abortControllerRef = useRef<AbortController>();
function destory() {
setData(undefined);
setLoading(false);
setError(null);
abortControllerRef.current && abortControllerRef.current.abort();
useEffect(() => {
setLoading(true);
abortControllerRef.current = new AbortController();
request(url, options || {}, abortControllerRef.current).then(res => {
const { code, message, data } = res as IResponseData;
if (code !== 0) {
console.log('Error Msg: ', message);
throw new Error(message);
setData(data);
setLoading(false);
}).catch(err => {
setError(err);
}).finally(() => {
setLoading(false);
return () => destory();
}, [url, JSON.stringify(options)]);
return { loading, data, error };
export default useFetchData;
代码就是上面那个样子,我们来看看是不是满足上面四点目标:
1 - 使用起来简单、统一,减少请求逻辑在业务组件里
// 1 - 基础使用 GET 无参数
const { loading, data } = useFetchData('/user/list');
// 2 - 进阶使用 GET 有参数
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number | undefined>(10);
const options = { query: { page, pageSize } };
const { loading, data } = useFetchData(getUserList, options);
// 3 - 进阶使用 POST
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number | undefined>(10);
const options = useMemo(() => ({
method: EHttpMethods.POST,
data: { page, pageSize }
}), [page, pageSize]);
const { loading, data } = useFetchData(postUserList, options);
可以看到,使用上应该可以算是清晰简单,你只需要传递对应请求的参数和 api 地址即可获取到数据以及 loading 状态。并且请求的逻辑在业务组件里只有必要的 api url 和参数,这两个是无法减少的,至于其他逻辑,完全封装到了 hooks 内部。
这里需要注意的是,因为 options 也就是 hooks 的第二个参数 是一个对象,因此会存在一个问题,就是组件重复渲染的时候,hooks 会重复发请求,因为每一次 options 都是一个新对象,即使没有改变也是一个新的内存地址,所以为了避免这种情况,有两种解决方案。
第一种:hooks 层解决,我这边使用了 useEffect + JSON.stringify 来确保参数发生变化才重新请求。
第二种:业务层解决,业务代码使用 useMemo 来进行处理。
个人而言更倾向第二种,目前因为是 Demo 阶段,所以两种方法我里面都用了,大家按需使用即可。
2 - hooks 内部进行错误处理
错误处理集中在请求库和 hooks 内部,业务组件不需要关心以及处理错误,大大减少了业务代码复杂度,核心代码如下:
// 请求层错误处理
* 错误处理
* @param err
* @param abortController
function dealErrToast(err: Error & ICustomRequestError, abortController?: AbortController) {
switch(err.status) {
case 408: {
abortController && abortController.abort();
(typeof window !== 'undefined') && message.error(err.statusText);
break;
default: {
console.log(err);
break;
// hooks 层错误处理
const { code, message, data } = res as IResponseData;
if (code !== 0) {
// do something
console.log('Error Msg: ', message);
throw new Error(message);
返回响应数据和 loading + TS 增强数据提示
这个看代码就很简单了,每一个请求都返回了响应的数据以及 loading 状态,同样的大大程度精简了业务代码,非常简便。
这里可以说明一下,为了清晰明了,我在 hooks 内部增设了 loading 这个 state,其实可以通过 data 和 error 两个字段聚合出 loading 状态,这里大家可以参考 swr。不过我觉得增加一个 loading 更清晰,所以就这么写了。
接下来就是 TS 的好处了,如果你在数据模型里设置里返回的数据类型,那么你获取到的 data 就是一个带提示的数据,在业务开发里非常的好用,只需要在编辑器里 . 一下,就能看到返回的这个数据对象的各种属性,开发简直不要太爽,再也不用边开发边查阅 API 文档了!效果如下图:
具体代码如下:
// 数据接口层
export interface IUserStruct {
id: number;
name: string;
age: number;
export interface IUserListResData {
list: IUserStruct[];
total: number
// 业务代码层
const { loading, data } = useFetchData<IUserListResData>(getLimitUserList, options);
最后你的 data 就如上图所示了~
这篇文章也算是个人想总结想写很久的一篇文章了,毕竟我真的很喜欢用 fetch,但是在公司里很少有人用,所以意难平吧。本文说技术含量其实也没啥技术含量,更多的应该是经验分享吧,代码较多,建议大家 Clone 仓库自己跑一跑,希望对大家有用处。