手写一个自己属于自己的useRequset的hook

38 阅读3分钟

1、创建一个useRequs文件并暴露方法

// useRequest是一个implement

import useRequestImplement from './useRequestImplement';

import type { Service, Options, Plugin } from './types';

function useRequest<TData, TParams extends any[]>(
    service: Service<TData, TParams>,
    options?: Options<TData, TParams>,
    plugins?: Plugin<TData, TParams>[],
){
    return useRequestImplement<TData, TParams>(service, 
        options, [...(plugins || [])] as Plugin<TData, TParams>[] );
}
export default useRequest;

2、定义TS的type属性

import type Fetch from './Fetch';


export type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;
export type Subscribe = () => void;
export interface CachedData<TData = any, TParams = any> {
    data: TData;
    params: TParams;
    time: number;
}

export interface Options<TData, TParams extends any[]> {
    manual?: boolean;

    onBefore?: (params: TParams) => void;
    onSuccess?: (data: TData, params: TParams) => void;
    onError?: (e: Error, params: TParams) => void;
    // formatResult?: (res: any) => TData;
    onFinally?: (params: TParams, data?: TData, e?: Error) => void;

    defaultParams?: TParams;

    // refreshDeps
    refreshDeps?: DependencyList;
    refreshDepsAction?: () => void;

    // loading delay
    loadingDelay?: number;

    // polling
    pollingInterval?: number;
    pollingWhenHidden?: boolean;
    pollingErrorRetryCount?: number;

    // refresh on window focus
    refreshOnWindowFocus?: boolean;
    focusTimespan?: number;

    // debounce
    debounceWait?: number;
    debounceLeading?: boolean;
    debounceTrailing?: boolean;
    debounceMaxWait?: number;

    // throttle
    throttleWait?: number;
    throttleLeading?: boolean;
    throttleTrailing?: boolean;

    // cache
    cacheKey?: string;
    cacheTime?: number;
    staleTime?: number;
    setCache?: (data: CachedData<TData, TParams>) => void;
    getCache?: (params: TParams) => CachedData<TData, TParams> | undefined;

    // retry
    retryCount?: number;
    retryInterval?: number;

    // ready
    ready?: boolean;

    // [key: string]: any;
}
export interface FetchState<TData, TParams extends any[]> {
    loading: boolean;
    params?: TParams;
    data?: TData;
    error?: Error;
}

export type Plugin<TData, TParams extends any[]> = {
    (fetchInstance: Fetch<TData, TParams>, options: Options<TData, TParams>): PluginReturn<
        TData,
        TParams
    >;
    onInit?: (options: Options<TData, TParams>) => Partial<FetchState<TData, TParams>>;
};

export interface PluginReturn<TData, TParams extends any[]> {
    onBefore?: (params: TParams) =>
        | ({
            stopNow?: boolean;
            returnNow?: boolean;
        } & Partial<FetchState<TData, TParams>>)
        | void;

    onRequest?: (
        service: Service<TData, TParams>,
        params: TParams,
    ) => {
        servicePromise?: Promise<TData>;
    };

    onSuccess?: (data: TData, params: TParams) => void;
    onError?: (e: Error, params: TParams) => void;
    onFinally?: (params: TParams, data?: TData, e?: Error) => void;
    onCancel?: () => void;
    onMutate?: (data: TData) => void;
}

3、实现一个Fetch方法

import { Service, Options, Subscribe, FetchState, PluginReturn } from './types';
import { isFunction } from './endcodeHooks'
// fetch 构造函数
export default class Fetch<TData, TParams extends any[]> {
    pluginImpl: PluginReturn<TData, TParams>[] | undefined;
    count: number = 0;

    state: FetchState<TData, TParams> = {
        loading: false,
        params: undefined,
        data: undefined,
        error: undefined,
    };

    constructor(
        public serviceRef: MutableRefObject<Service<TData, TParams>>,
        public options: Options<TData, TParams>,
        public subscribe: Subscribe,
        public initState: Partial<FetchState<TData, TParams>> = {},
    ) {
        this.state = {
            ...this.state,
            loading: !options.manual,
            ...initState,
        };
    }

    setState(s: Partial<FetchState<TData, TParams>> = {}) {
        this.state = {
            ...this.state,
            ...s,
        };
        this.subscribe();
    }

    runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
        // @ts-ignore
        const r = this.pluginImpl.map((i) => i[event]?.(...rest)).filter(Boolean);
        return Object.assign({}, ...r);
    }

    async runAsync(...params: TParams): Promise<TData> {
        this.count += 1;
        const currentCount = this.count;

        const {
            stopNow = false,
            returnNow = false,
            ...state
        } = this.runPluginHandler('onBefore', params);

        // stop request
        if (stopNow) {
            return new Promise(() => { });
        }

        this.setState({
            loading: true,
            params,
            ...state,
        });

        // return now
        if (returnNow) {
            return Promise.resolve(state.data);
        }

        this.options.onBefore?.(params);

        try {
            // replace service
            let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

            if (!servicePromise) {
                servicePromise = this.serviceRef.current(...params);
            }

            const res = await servicePromise;

            if (currentCount !== this.count) {
                // prevent run.then when request is canceled
                return new Promise(() => { });
            }

            // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;

            this.setState({
                data: res,
                error: undefined,
                loading: false,
            });

            this.options.onSuccess?.(res, params);
            this.runPluginHandler('onSuccess', res, params);

            this.options.onFinally?.(params, res, undefined);

            if (currentCount === this.count) {
                this.runPluginHandler('onFinally', params, res, undefined);
            }

            return res;
        } catch (error) {
            if (currentCount !== this.count) {
                // prevent run.then when request is canceled
                return new Promise(() => { });
            }

            this.setState({
                error,
                loading: false,
            });

            this.options.onError?.(error, params);
            this.runPluginHandler('onError', error, params);

            this.options.onFinally?.(params, undefined, error);

            if (currentCount === this.count) {
                this.runPluginHandler('onFinally', params, undefined, error);
            }

            throw error;
        }
    }

    run(...params: TParams) {
        this.runAsync(...params).catch((error) => {
            if (!this.options.onError) {
                console.error(error);
            }
        });
    }

    cancel() {
        this.count += 1;
        this.setState({
            loading: false,
        });

        this.runPluginHandler('onCancel');
    }

    refresh() {
        // @ts-ignore
        this.run(...(this.state.params || []));
    }

    refreshAsync() {
        // @ts-ignore
        return this.runAsync(...(this.state.params || []));
    }

    mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
        const targetData = isFunction(data) ? data(this.state.data) : data;
        this.runPluginHandler('onMutate', targetData);
        this.setState({
            data: targetData,
        });
    }
}

4、实现一个自定义的endcodeHooks

import { useEffect, useRef, useState, useCallback } from 'react';
type noop = (this: any, ...args: any[]) => any;
type PickFunction<T extends noop> = (
    this: ThisParameterType<T>,
    ...args: Parameters<T>
) => ReturnType<T>
export const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';

export function useLatest<T>(value: T) {
    const ref = useRef(value);
    ref.current = value;
    return ref;
}

export function useMount(fn: () => void) {
    if (isDev) {
        if (!isFunction(fn)) {
            console.error(
                `useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`,
            );
        }
    }

    useEffect(() => {
        fn?.();
    }, []);
};

export function useMemoizedFn<T extends noop>(fn: T) {
    //  根据fn创建 ref
    const fnRef = useRef<T>(fn);
    fnRef.current = fn;
    // 创建一个useMemoizedFn
    const memoizedRef = useRef<PickFunction<T>>();
    if (!memoizedRef.current) {
        memoizedRef.current = function (this, ...args) {
            // apply
            return fnRef.current.apply(this, args);
        }
    }
    return memoizedRef.current;
}

export function useUpdate() {
    const [, setState] = useState({});

    return useCallback(() => setState({}), []);
};

export function useUnmount(fn: () => void) {
    if (isDev) {
        if (!isFunction(fn)) {
            console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);
        }
    }
    const fnRef = useLatest(fn);
    useEffect(
        () => () => {
            fnRef.current();
        },
        [],
    );
};

5、实现一个useRequset的实例方法useRequestImplement

import Fetch from './Fetch'
import { useLatest, useMount, useMemoizedFn, useUpdate, useUnmount } from './endcodeHooks';
import type { Service, Options, Plugin } from './types';


function useRequestImplement<TData, TParams extends any[]>(
    service: Service<TData, TParams>,
    options?: Options<TData, TParams>,
    plugins?: Plugin<TData, TParams>[],
){
    // 针对fetch hooks options 管控请求
    const { manual = false, ...rest } = options;
    // 创建一个options作为透传
    const fetchOptions = {
        manual,
        ...rest
    };
    // 实现一个显示声明
    const serviceRef = useLatest(service);
    const update = useUpdate();
    const initState = plugins?.map((plugin) => plugin?.onInit?.(fetchOptions)).fillter(Boolean);
    const fetchInstance = new Fetch<TData, TParams>(
        serviceRef,
        update,
        initState,
        Object.assign({}, ...initState),
    )
    // 针对请求,如果非manual 
    useMount(()=>{
        if(!manual){
            fetchInstance.run(
                ...((fetchInstance.state.params || options?.defaultParams || []) as TParams)
            )
        }
    });
    useUnmount(()=>{
        fetchInstance.cancel();
    })
    return {
        loading: fetchInstance.state.loading,
        data: fetchInstance.state.data,
        error: fetchInstance.state.error,
        params: fetchInstance.state.params,
        run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
        runAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
        refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
        cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
    }

};

export default useRequestImplement

就是完成一个自身useRequset请求实现 7、使用demo

import { useResetState } from 'encode-hooks';

interface State {
  hello: string;
  count: number;
}

export App =() => {
  const [state, setState, resetState] = useResetState<State>({
    hello: '',
    count: 0,
  });

  return (
    <div>
      <pre>{JSON.stringify(state, null, 2)}</pre>
      <p>
        <button
          type="button"
          style={{ marginRight: '8px' }}
          onClick={() => setState({ hello: 'world', count: 1 })}
        >
          set hello and count
        </button>

        <button type="button" onClick={resetState}>
          resetState
        </button>
      </p>
    </div>
  );
};