? ? ? ? 当我们使用useState和请求时,有这样的场景: 需要在页面挂载的时候请求,并且支持loading状态和错误信息捕获。 代码逻辑不难,我们可能写出如下的代码
const [data, setData] = useState()//数据
const [err, setErr] = useState<unknown>()//错误信息
const [loading, setLoading] = useState(false)//是否正在加载
const getData = async () => {
try {
setLoading(true)
const res = await xxxx()
setData(res)
} catch (error) {
setErr(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
getData()
}, [])
? ? ? ? 但是如果很多页面都有这样的需求,那我们就需要考虑抽离出来封装,因为不可能每个页面都去复制粘贴这样一份代码吧?
? ? ? ? 下面来实现一个useRequest,主打一个高复用性,且TypeScript支持度高,类型自动推断如何传递。
/**任意参数个数,任意返回值的async函数 */
export type AnyPromiseFn = (...param: any[]) => Promise<any>
/**获得一个async函数的返回值,ReturnType得到的是Promise< D > ,返回D */
export type GetPromiseReturnType<T extends AnyPromiseFn> = ReturnType<T> extends Promise<infer D> ? D : never
/**useRequest 参数的类型 */
type UseRequestAllParam<T extends AnyPromiseFn> = {
/**请求函数 */
reqFn: T,
/**useState初始值 */
initVal: GetPromiseReturnType<T>
/**给请求函数,在初始化时传递的参数 (用元组形式传递) */
params: Parameters<T>;
/**请求完成后,对数据的处理函数,每次请求后都会执行这个 (比如可以在这里利用数据做其他事) */
afterReq?: (data: GetPromiseReturnType<T>) => Promise<void> | void
}
/**根据T的参数情况,动态判断是否需要传递param参数 */
type UseRequestParam<T extends AnyPromiseFn> = Parameters<T>['length'] extends 0 ? Omit<UseRequestAllParam<T>, 'params'> : UseRequestAllParam<T>
import { useEffect, useState } from "react";
/**请求hook,在页面挂载时请求,自带loading状态和错误状态,同时返回函数支持二次调用
* @param options 传递一个配置参数
* @param options.reqFn 请求函数
* @param options.initVal useState初始值
* @param options.params 初始化时,给请求函数传递的参数,以元组形式传递 (如果请求函数没参数则不用填)
* @param options.afterReq 请求完成后,对数据的处理函数,每次请求后都会执行这个 (比如可以在这里利用数据做其他事)
* @returns [data, loading, getData, err, setData, setLoading, setErr]
*/
export default function useRequest<T extends AnyPromiseFn>(options: UseRequestParam<T>) {
const { reqFn, params = [], afterReq, initVal } = options as UseRequestAllParam<T>
const [data, setData] = useState(initVal)//数据
const [err, setErr] = useState<unknown>()//错误信息
const [loading, setLoading] = useState(false)//是否正在加载
/**封装好了的请求函数,可以用于再次请求 */
const getData = async (...params: Parameters<T>) => {
try {
setLoading(true)
const res = params ? await reqFn(...params) : await reqFn()
setData(res)
afterReq?.(res)
} catch (error: any) {
setErr(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
getData(...params as Parameters<T>)
}, [])
return [
data,
loading,
getData,
err,
//下面这些也返回出去,有可能会被用到
setData,
setLoading,
setErr,
] as [typeof data, typeof loading, typeof getData, typeof err, typeof setData, typeof setLoading, typeof setErr] //返回一个元组类型,暂时没有想到更好的方法来支持返回值的类型推导
}
type TestReq1 = { p: string };
type TestReq2 = string;
type TestRes = string[];
//在给 reqFn 传入了函数之后, params的类型就被限制为 [TestReq1, TestReq2] 元组了, initVal的类型则被限制为 TestRes, 实现TS类型限制
const [data, loading, getData] = useRequest({
reqFn: async (param1: TestReq1, param2: TestReq2): Promise<TestRes> => await request.get("/xxxx", {param1, param2}) ,
params: [{ p: "3d" }, "第二个参数"],
initVal: [],
});
//当 reqFn 没有参数时,则不需要传递 params 属性, 实现TS自动判断是否需要传参
const [data2, loading2, getData2] = useRequest({
reqFn: async () => ({}),//返回一个对象
initVal: {}
});
//以上拿到的 getData 函数,可以被调用,参数类型和原函数相同
getData({ p: "测试重新使用-参数1" }, "测试重新使用-参数2")
getData2()
? ? ? ? 今天实现了useRequest函数,这个函数本身是没什么难度的,主要是需要支持TypeScript的类型推导,就当做一次TypeScript类型的练习吧