import { useCallback, useEffect, useMemo, useRef, useState, } from '@wordpress/element'; import debounce from 'lodash.debounce'; import isFunction from 'lodash.isfunction'; import throttle from 'lodash.throttle'; type RequestFn = (params: TParams) => Promise; type RequestMutateParam = | TData | ((oldData?: TData) => TData | undefined); const createRequestToken = () => { return Date.now(); }; export type UseRequestOptions = { ready?: boolean; manual?: boolean; params?: TParams; refreshDeps?: any[]; loadingDelay?: number; debounceWait?: number; throttleWait?: number; onBefore?: (params: TParams) => void; onSuccess?: (params: TParams, data: TData) => void; onError?: (err: Error, params: TParams) => void; onFinally?: (params: TParams, data?: TData, err?: Error) => void; }; export const useRequest = ( requestFn: RequestFn, options: UseRequestOptions = {}, ) => { const { params, ready = true, manual = false, refreshDeps = [], debounceWait, throttleWait, loadingDelay, onBefore, onSuccess, onError, onFinally, } = options; const [data, setData] = useState(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const requestToken = useRef(null); const cancelToken = useRef(null); const loadingDelayTimer = useRef(); const prevParams = useRef(params!); const validIsCancelled = useCallback((token: number) => { if (requestToken.current && cancelToken.current === requestToken.current) { setLoading(false); return true; } if (requestToken.current && token !== requestToken.current) { return true; } return false; }, []); const request = useCallback( (reqPrams: TParams, token: number) => { if (!ready) { return; } onBefore?.(reqPrams); if (validIsCancelled(token)) { return; } requestFn(reqPrams) .then(resData => { if (validIsCancelled(token)) { return; } setData(resData); onSuccess?.(reqPrams, resData); onFinally?.(reqPrams, resData); }) .catch(reason => { if (validIsCancelled(token)) { return; } const reqError = new Error(reason); setError(reqError); onError?.(reqError, reqPrams); onFinally?.(reqPrams, undefined, reqError); }) .finally(() => { if (token === requestToken.current) { loadingDelayTimer.current && clearTimeout(loadingDelayTimer.current); loadingDelayTimer.current = undefined; setLoading(false); } }); }, [ready], ); const throttleRequest = useMemo( () => throttle((reqParams: TParams, token: number) => { if (validIsCancelled(token)) { return; } request(reqParams, token); }, throttleWait), [request, throttleWait], ); const debounceRequest = useMemo( () => debounce((reqParams: TParams, token: number) => { if (validIsCancelled(token)) { return; } request(reqParams, token); }, debounceWait), [request, debounceWait], ); const execRequest = useCallback( (reqPrams: TParams) => { const token = createRequestToken(); requestToken.current = token; loadingDelayTimer.current && clearTimeout(loadingDelayTimer.current); loadingDelayTimer.current = setTimeout(() => { if (cancelToken.current !== token) { setLoading(true); } }, loadingDelay); prevParams.current = reqPrams; if (throttleWait) { throttleRequest(reqPrams, token); } else if (debounceWait) { debounceRequest(reqPrams, token); } else { request(reqPrams, token); } }, [ request, throttleRequest, debounceRequest, throttleWait, debounceWait, loadingDelay, ], ); const run = useCallback( (runParams?: TParams | ((params: TParams) => TParams)) => { if (isFunction(runParams)) { execRequest(runParams(prevParams.current)); } else { execRequest((runParams || params)!); } }, [params, execRequest], ); const refresh = useCallback(() => { execRequest(prevParams.current); }, [execRequest]); const cancel = useCallback(() => { if (requestToken.current) { cancelToken.current = requestToken.current; } }, []); const mutate = useCallback((mutateParam: RequestMutateParam) => { if (isFunction(mutateParam)) { setData(mutateParam(data)); } else { setData(mutateParam); } }, []); useEffect(() => { if (!manual && ready) { execRequest(params!); } }, [...refreshDeps, ready]); useEffect(() => { return cancel; }, []); return { data, loading, error, run, refresh, cancel, mutate, }; };