React - 实现一个支持TypeScript类型推导的 useRequest

发布时间:2023年12月18日

一、引入

? ? ? ? 当我们使用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支持度高,类型自动推断如何传递。

二、代码

1. TS类型定义


/**任意参数个数,任意返回值的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>

?2.useRequest

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] //返回一个元组类型,暂时没有想到更好的方法来支持返回值的类型推导
}

3. 使用示例

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类型的练习吧

文章来源:https://blog.csdn.net/m0_64130892/article/details/134998192
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。