1. The origin of

Recently in my company’s project, I found that many of my colleagues were using the Ahooks useRequest. Seeing that this custom hook is really useful, IT inspired me to learn how it is implemented. And the official implementation ideas and code are very clear and worth learning. Through their own learning also want to share with you, together to understand the implementation of useRequest.

The following source code is based on ahooks 3.1.2

Let’s get started!

2. Implementation principle

2.1 usage

 // Basic usage
 const { data, error, loading } = useRequest(service);
 ​
 // Manual control
 const { loading, run, runAsync } = useRequest(service, {
   manual: true
 });
 ​
 <button onClick={run} disabled={loading}>
   {loading ? 'Loading' : 'Edit'}
 </button>
Copy the code
  • See useRequest Basic Usage for more details

2.2 Functions

Function of 1.

  • UseRequest is a powerful asynchronous data management Hooks that will suffice for network request scenarios in the React project. UseRequest organizes the code in a plug-in manner. The core code is extremely simple and can be easily extended to more advanced functions. Current capabilities include:

    • Automatic request/manual request
    • polling
    • Image stabilization
    • The throttle
    • Screen focus rerequested
    • Error retry
    • loading delay
    • SWR(stale-while-revalidate)
    • The cache

Of course, we can also write our own plug-ins to achieve different functions, currently the official provided these existing functions.

2. Plug-in life cycle

  • A review of the source code shows that useRequest’s plug-in has the following life cycles (and the corresponding plug-ins used) :

    • onInitTrigger — useAutoRunPlugin on initialization
    • onBeforeExecute before the request — useAutoRunPlugin, useCachePlugin, useLoadingDelayPlugin, usePollingPlugin, useRetryPlugin
    • onRequestInitiate a request — useCachePlugin
    • onSuccessTriggered when the request succeeds — useCachePlugin, useRetryPlugin
    • onErrorTriggered when the request fails — useRetryPlugin
    • onFinallyTriggered when the request completes (similar to finally) — useLoadingDelayPlugin, usePollingPlugin
    • onCancelTriggered when the request is cancelled — useDebouncePlugin, useLoadingDelayPlugin, usePollingPlugin, useRetryPlugin, useThrottlePlugin
    • onMutateThe — useCachePlugin is triggered when the returned data is manually modified

3. Pre-knowledge

UseRequest also uses other custom hooks in the source code

UseCreation: useCreation is an alternative to useMemo or useRef. UseLatest: a Hook that returns the current latest value. UseMemoizedFn: PersistentfunctionHookIn theory, it can be useduseMemoizedFnCompletely replaceuseCallback.useMount: executes only when the component is initializedHook.useUnmount: Component uninstallation (unmount)Hook.useUpdate:useUpdateReturns a function that forces the component to rerender.Copy the code

2.3 Code Implementation

1. Call the procedure

  1. First when we call useRequest
 // In fact, this is the only part of the useRequest initialization code, because this time the official functions are extracted as plugins to implement
 function useRequest<TData.TParams extends any[] > (service: Service
       
        , options? : Options
        
         , plugins? : Plugin
         
          [],
         ,>
        ,>
       ,>) {
   return useRequestImplement<TData, TParams>(service, options, [
     ...(plugins || []), // We can also pass in our own plugin
     useDebouncePlugin, / / image stabilization
     useLoadingDelayPlugin, // State of lazy loading
     usePollingPlugin, / / training in rotation
     useRefreshOnWindowFocusPlugin, // Rerequest window focus
     useThrottlePlugin, / / throttling
     useAutoRunPlugin, // Automatic requests based on changes in ready
     useCachePlugin, / / cache
     useRetryPlugin, // Error retry
 ])
Copy the code
  • Methods of concrete instantiation
 function useRequestImplement<TData.TParams extends any[] > (service: Service
       
        , options: Options
        
          = {}, plugins: Plugin
         
          [] = [],
         ,>
        ,>
       ,>) {
   // The request is automatically sent by default
   const { manual = false. rest } = options;constfetchOptions = { manual, ... rest, };// Save a reference to the latest request method
   const serviceRef = useLatest(service);
   / / update
   const update = useUpdate();
   // Create request instance
   const fetchInstance = useCreation(() = > {
     // Run the onInit method for each plug-in
     const initState = plugins.map((p) = >p? .onInit? .(fetchOptions)).filter(Boolean);
     return new Fetch<TData, TParams>(
       serviceRef,
       fetchOptions,
       update,
       Object.assign({}, ...initState),
     );
   }, []);
   fetchInstance.options = fetchOptions;
   // Run all plugins
   fetchInstance.pluginImpls = plugins.map((p) = > p(fetchInstance, fetchOptions));
 ​
   // Initiate a request if the mount is automatic
   useMount(() = > {
     if(! manual) {// useCachePlugin can set fetchInstance.state.params from cache when init
       const params = fetchInstance.state.params || options.defaultParams || [];
       fetchInstance.run(...params);
     }
   });
 ​
   // Cancel the request while uninstalling
   useUnmount(() = > {
     fetchInstance.cancel();
   });
 ​
   // Return data
   return {
     loading: fetchInstance.state.loading,
     data: fetchInstance.state.data,
     error: fetchInstance.state.error,
     params: fetchInstance.state.params || [],
     cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
     refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
     refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
     run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
     runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
     mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
   };
 }
Copy the code
  • So here we see the onInit life cycle called
// Run the onInit method for each plug-in const initState = plugins.map((p) => p? .onInit? .(fetchOptions)).filter(Boolean); Useautorunplugin. onInit = ({ready = true, manual }) => { return { loading: ! manual && ready, }; };Copy the code

UseRequest provides an options.ready parameter. When its value is false, the request will never be sent.

  • Its specific behavior is as follows:

    • whenmanual=falseIn automatic request mode, every time ready changes from false to true, the request is automatically initiated with the options.defaultParams parameter.
    • whenmanual=trueIn manual request mode, requests triggered by RUN /runAsync will not be executed as long as Ready =false.
  1. Instantiate the request
 export default class Fetch<TData.TParams extends any[] >{
   // All plug-ins
   pluginImpls: PluginReturn<TData, TParams>[];
   / / counter
   count: number = 0;
   // Initial data
   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 is determined by the return state of onInit (useAutoRunPlugin) in initState
       loading:! options.manual, ... initState, }; }React class setState = react class setState = react class setState
   setState(s: Partial<FetchState<TData, TParams>> = {}) {
     this.state = { ... this.state, ... s, };this.subscribe();
   }
   // Define a public method that calls the plug-in xx lifecycle
   runPluginHandler(event: keyof PluginReturn<TData, TParams>, ... rest:any[]) {
     // @ts-ignore
     const r = this.pluginImpls.map((i) = >i[event]? . (... rest)).filter(Boolean);
     return Object.assign({}, ... r); }// Execute the request, which is also deconstructed when we use run
   run(. params: TParams) {
     // Call the runAsync implementation
     this.runAsync(... params)// This is why run automatically catches exceptions
       .catch((error) = > {
       if (!this.options.onError) {
         console.error(error); }}); }// Cancel the request
   cancel() {
     this.count += 1;
     this.setState({
       loading: false});// Call the plug-in's onCancel method
     this.runPluginHandler('onCancel');
   }
  
   // Refresh is a new request
   refresh() {
     this.run(... (this.state.params || []));
   }
   
   / / same as above
   refreshAsync() {
     return this.runAsync(... (this.state.params || []));
   }
   
   // Manually change the returned data
   mutate(data? : TData | ((oldData? : TData) => TData |undefined)) {
     let targetData: TData | undefined;
     if (typeof data === 'function') {
       targetData = data(this.state.data);
     } else {
       targetData = data;
     }
     // Call the plugin's onMutate method
     this.runPluginHandler('onMutate', targetData);
     this.setState({
       data: targetData,
     });
   }
   
   // This method is where all the logic is really handled, so take it out on its own
   runAsync(){...}
   
 }
Copy the code
  1. The realization of the runAsync
 asyncrunAsync(... params: TParams):Promise<TData> {
     // count +1
     this.count += 1;
     const currentCount = this.count;
 ​
     const {
       stopNow = false./ /! ready return true
       returnNow = false.// If the cache is available. state// If there is a cache, the value here will be set to the cached value (whether expired or not)
     } = this.runPluginHandler('onBefore', params);
 ​
     // stop request
     if (stopNow) {
       return new Promise(() = > {});
     }
 ​
     this.setState({
       loading: true, params, ... state, });// Use cache
     if (returnNow) {
       return Promise.resolve(state.data);
     }
     // call your own onBefore
     this.options.onBefore? .(params);try {
       // replace service
       // More on caching later
       let { servicePromise } = this.runPluginHandler('onRequest'.this.serviceRef.current, params);
 ​
       if(! servicePromise) {// The service passed by the caller
         servicePromise = this.serviceRef.current(... params); }const res = await servicePromise;
       
       // Here count is +1 for each run and cancel. If cancel is not called before the request, the count is equal for both runs
       if(currentCount ! = =this.count) {
         // prevent run.then when request is canceled
         return new Promise(() = > {});
       }
       
       // Return the requested data
       this.setState({
         data: res,
         error: undefined.loading: false});// Invoke the onSuccess lifecycle
       this.options.onSuccess? .(res, params);this.runPluginHandler('onSuccess', res, params);
       
       // Call the onFinally lifecycle
       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});// Call the onError lifecycle
       this.options.onError? .(error, params);this.runPluginHandler('onError', error, params);
       
       // Call the onFinally lifecycle
       this.options.onFinally? .(params,undefined, error);
       if (currentCount === this.count) {
         this.runPluginHandler('onFinally', params, undefined, error);
       }
 ​
       throwerror; }}Copy the code
  • foronRequestThe life cycle is onlyuseCachePluginSo let’s see
 const currentPromiseRef = useRef<Promise<any> > ();// If caching is used, each service will also be cached
 useCachePlugin.onRequest = (service, args) = > {
   let servicePromise = cachePromise.getCachePromise(cacheKey);
   // If has servicePromise, and is not trigger by self, then use it
   if(servicePromise && servicePromise ! == currentPromiseRef.current) {return{ servicePromise }; } servicePromise = service(... args); currentPromiseRef.current = servicePromise; cachePromise.setCachePromise(cacheKey, servicePromise);return { servicePromise };
 };
Copy the code
summary
  • Above is the main flow, you can see that in the main flow, in addition to calling the life cycle of various plug-ins, there is also a request based on conditions. There is nothing else. Because this version management put all the functions into each plug-in to implement, this not only makes the code easier to understand, and each plug-in is responsible for one thing is also convenient for users to extend the function.

2. Functions of the plug-in

1. Automatic request/manual request

As we have seen above, if manual is false the request will be automatically initiated, otherwise the user needs to trigger it

2. useAutoRunPlugin
 import { useRef } from 'react';
 import useUpdateEffect from '.. /.. /.. /useUpdateEffect';
 ​
 // support refreshDeps & ready
 const useAutoRunPlugin: Plugin<any.any[] > =(
   fetchInstance,
   // Options passed by the user
   { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
 ) = > {
   const hasAutoRun = useRef(false);
   hasAutoRun.current = false;
 ​
   // useEffect
   useUpdateEffect(() = > {
     // In manual=false automatic request mode, a request is automatically initiated each time ready changes from false to true
     if(! manual && ready) { hasAutoRun.current =true; fetchInstance.run(... defaultParams); } }, [ready]); useUpdateEffect(() = > {
     if (hasAutoRun.current) {
       return;
     }
     if(! manual) { hasAutoRun.current =true;
       if (refreshDepsAction) {
         refreshDepsAction();
       } else {
         fetchInstance.refresh();
       }
     }
   }, [...refreshDeps]); // You can customize deps yourself, and then you can handle callbacks yourself
 ​
   return {
     onBefore: () = > {
       if(! ready) {return {
           stopNow: true}; }}}; }; useAutoRunPlugin.onInit =({ ready = true, manual }) = > {
   return {
     loading: !manual && ready,
   };
 };
 ​
 export default useAutoRunPlugin;
Copy the code
3. useLoadingDelayPlugin
 import { useRef } from 'react';
 // By setting options.loadingDelay, you can delay the time when loading becomes true to prevent blinking.
 // The whole idea is to use setTimeout to delay loading to true
 const useLoadingDelayPlugin: Plugin<any.any[] > =(fetchInstance, { loadingDelay }) = > {
   const timerRef = useRef<Timeout>();
 ​
   if(! loadingDelay) {return {};
   }
 ​
   const cancelTimeout = () = > {
     if (timerRef.current) {
       clearTimeout(timerRef.current); }};return {
     onBefore: () = > {
       cancelTimeout();
       
       // The main implementation
       timerRef.current = setTimeout(() = > {
         fetchInstance.setState({
           loading: true}); }, loadingDelay);return {
         loading: false}; },onFinally: () = > {
       cancelTimeout();
     },
     onCancel: () = >{ cancelTimeout(); }}; };export default useLoadingDelayPlugin;
Copy the code
4. useDebouncePlugin
 import type { DebouncedFunc, DebounceSettings } from 'lodash';
 import debounce from 'lodash/debounce';
 import { useEffect, useMemo, useRef } from 'react';
 ​
 // Enter the anti-shake mode by setting options.debounceWait
 // This is implemented using lodash's debounce
 const useDebouncePlugin: Plugin<any.any[] > =(fetchInstance, { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },) = > {
   const debouncedRef = useRef<DebouncedFunc<any> > ();// Because these parameters support dynamic change
   const options = useMemo(() = > {
     const ret: DebounceSettings = {};
     if(debounceLeading ! = =undefined) {
       ret.leading = debounceLeading;
     }
     if(debounceTrailing ! = =undefined) {
       ret.trailing = debounceTrailing;
     }
     if(debounceMaxWait ! = =undefined) {
       ret.maxWait = debounceMaxWait;
     }
     return ret;
   }, [debounceLeading, debounceTrailing, debounceMaxWait]);
 ​
   useEffect(() = > {
     // Turn on the anti-shake function
     if (debounceWait) {
       // There is an interception of the original method
       const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
       // debounce runAsync should be promise
       // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
       // Based on the issue above, the following is used to deal with the debounce return promise problem
       fetchInstance.runAsync = (. args) = > {
         return new Promise((resolve, reject) = >{ debouncedRef.current? . (() = >{ _originRunAsync(... args) .then(resolve) .catch(reject); }); }); }; debouncedRef.current = debounce((callback) = > {
           callback();
         },
         debounceWait,
         options,
       );
 ​
       return () = >{ debouncedRef.current? .cancel();// Cancel interception
         fetchInstance.runAsync = _originRunAsync;
       };
     }
   }, [debounceWait, options]);
 ​
   if(! debounceWait) {return {};
   }
 ​
   return {
     onCancel: () = >{ debouncedRef.current? .cancel(); }}; };export default useDebouncePlugin;
Copy the code
5. useThrottlePlugin

Same principle as above, using Lodash throttle

6. usePollingPlugin
 import { useRef } from 'react';
 import useUpdateEffect from '.. /.. /.. /useUpdateEffect';
 import isDocumentVisible from '.. /utils/isDocumentVisible';
 import subscribeReVisible from '.. /utils/subscribeReVisible';
 ​
 // Set options.pollingInterval to enter the polling mode
 // The main implementation idea is to use setTimeout to implement polling. Polling principle is to wait for the pollingInterval time after each request is completed to launch the next request.
 const usePollingPlugin: Plugin<any.any[] > =(
   fetchInstance,
   { pollingInterval, pollingWhenHidden = true /* Whether to continue polling while the page is hidden */ },
 ) = > {
   const timerRef = useRef<Timeout>();
   const unsubscribeRef = useRef<() = > void> ();// Stop polling clearTimeout
   const stopPolling = () = > {
     if (timerRef.current) {
       clearTimeout(timerRef.current); } unsubscribeRef.current? . (); };// Dynamic change is supported
   useUpdateEffect(() = > {
     if(! pollingInterval) { stopPolling(); } }, [pollingInterval]);if(! pollingInterval) {return {};
   }
 ​
   return {
     onBefore: () = > {
       stopPolling();
     },
     // The onFinally lifecycle is called at the end of each request
     onFinally: () = > {
       // if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible
       if(! pollingWhenHidden && ! isDocumentVisible()) { unsubscribeRef.current = subscribeReVisible(() = > {
           fetchInstance.refresh();
         });
         return;
       }
 ​
       timerRef.current = setTimeout(() = > {
         fetchInstance.refresh();
       }, pollingInterval);
     },
     onCancel: () = >{ stopPolling(); }}; };export default usePollingPlugin;
 ​
 // isDocumentVisible.ts
 export default function isDocumentVisible() :boolean {
   if (canUseDom()) {
     return document.visibilityState ! = ='hidden';
   }
   return true;
 }
 ​
 // subscribeReVisible.ts
 // This is a subscription-like model that notifies the subscription component when visibilityChange is visible
 import isDocumentVisible from './isDocumentVisible';
 ​
 const listeners: any[] = [];
 function subscribe(listener: () => void) {
   listeners.push(listener);
   return function unsubscribe() {
     const index = listeners.indexOf(listener);
     listeners.splice(index, 1);
   };
 }
 ​
 if (canUseDom()) {
   const revalidate = () = > {
     if(! isDocumentVisible())return;
     for (let i = 0; i < listeners.length; i++) {
       constlistener = listeners[i]; listener(); }};window.addEventListener('visibilitychange', revalidate, false);
 }
 ​
 export default subscribe;
Copy the code
7. useRefreshOnWindowFocusPlugin
 import { useEffect, useRef } from 'react';
 import limit from '.. /utils/limit'; // It can be interpreted as throttling
 ​
 // Implement similar subscribeReVisible. Ts but with a focus event, judge whether the condition is isOnline (navigator.online)
 import subscribeFocus from '.. /utils/subscribeFocus';
 ​
 / / by setting options. RefreshOnWindowFocus, refocus and revisible, the browser window to initiate requests, support the dynamic change
 // Listen for browser events visibilitychange and focus
 const useRefreshOnWindowFocusPlugin: Plugin<any.any[] > =(
   fetchInstance,
   { refreshOnWindowFocus, focusTimespan = 5000 /* Rerequest interval, in milliseconds */ },
 ) = > {
   const unsubscribeRef = useRef<() = > void> ();const stopSubscribe = () = >{ unsubscribeRef.current? . (); }; useEffect(() = > {
     if (refreshOnWindowFocus) {
       const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
       unsubscribeRef.current = subscribeFocus(() = > {
         limitRefresh();
       });
     }
     return () = > {
       stopSubscribe();
     };
   }, [refreshOnWindowFocus, focusTimespan]);
 ​
   useUnmount(() = > {
     stopSubscribe();
   });
 ​
   return {};
 };
 ​
 export default useRefreshOnWindowFocusPlugin;
 ​
Copy the code
8. useRetryPlugin
 import { useRef } from 'react';
 ​
 // By setting options.retryCount to specify the number of error retries, useRequest will retry after a failure. Support dynamic change
 // When an error occurs, use setTimeout to set a different time (maximum 30s) for the request
 const useRetryPlugin: Plugin<any.any[] > =(fetchInstance, { retryInterval, retryCount }) = > {
   const timerRef = useRef<Timeout>();
   const countRef = useRef(0);
 ​
   const triggerByRetry = useRef(false);
 ​
   if(! retryCount) {return {};
   }
 ​
   return {
     onBefore: () = > {
       if(! triggerByRetry.current) {// Number of retry times
         countRef.current = 0;
       }
       triggerByRetry.current = false;
 ​
       if (timerRef.current) {
         clearTimeout(timerRef.current); }},onSuccess: () = > {
       countRef.current = 0;
     },
     onError: () = > {
       // Number of retries +1
       countRef.current += 1;
       // -1 indicates an unlimited number of retries
       if (retryCount === -1 || countRef.current <= retryCount) {
         // Exponential backoff
         const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
         timerRef.current = setTimeout(() = > {
           triggerByRetry.current = true;
           fetchInstance.refresh();
         }, timeout);
       } else {
         countRef.current = 0; }},onCancel: () = > {
       countRef.current = 0;
       if (timerRef.current) {
         clearTimeout(timerRef.current); }}}; };export default useRetryPlugin;
 ​
Copy the code
9. useCachePlugin
 import { useRef } from 'react';
 import * as cache from '.. /utils/cache';
 import * as cachePromise from '.. /utils/cachePromise';
 import * as cacheSubscribe from '.. /utils/cacheSubscribe';
 /** If options.cacheKey is set, useRequest will cache the current successful request. The next time a component initializes, if there is cached data, we will return cached data first, and then send a new request behind it, which is the ability of SWR. You can set the time for data to remain fresh via options.staleTime, during which time we consider the data to be fresh and will not re-initiate the request. You can also set data cacheTime with options.cacheTime, after which we will flush the cache. * /
 const useCachePlugin: Plugin<any.any[] > =(
   fetchInstance,
   {
     cacheKey,
     cacheTime = 5 * 60 * 1000,
     staleTime = 0,
     setCache: customSetCache, //Custom method getCache: customGetCache,//Custom methods that can store data tolocalStorageAnd IndexDB},) = > {
   const unSubscribeRef = useRef<() = > void> ();const currentPromiseRef = useRef<Promise<any> > ();// Set the cache to custom or default
   // In custom cache mode, cacheTime and clearCache do not take effect
   // No custom methods are passed to the developer at all
   const _setCache = (key: string, cachedData: CachedData) = > {
     if (customSetCache) {
       customSetCache(cachedData);
     } else {
       cache.setCache(key, cacheTime, cachedData);
     }
     cacheSubscribe.trigger(key, cachedData.data);
   };
   
   // Get cache to go custom or default
   const _getCache = (key: string, params: any[] = []) = > {
     if (customGetCache) {
       return customGetCache(params);
     }
     return cache.getCache(key);
   };
 ​
   useCreation(() = > {
     // There is no cacheKey Return
     if(! cacheKey) {return;
     }
 ​
     // get data from cache when init
     const cacheData = _getCache(cacheKey);
     if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
       // decache and assign to state
       fetchInstance.state.data = cacheData.data;
       fetchInstance.state.params = cacheData.params;
       // There is no out-of-date or fresh data
       if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
         fetchInstance.state.loading = false; }}// subscribe same cachekey update, trigger update
     /** Contents of the same cacheKey are shared globally, which brings the following features: 1. Requests and promises are shared. Only one cacheKey is used in a cacheKey, and subsequent requests share the same Promise. Data synchronization. Anytime we change the contents of one of the CacheKeys, the contents of the other cacheKeys are synchronized */
     unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (data) = >{ fetchInstance.setState({ data }); }); } []); useUnmount(() = >{ unSubscribeRef.current? . (); });if(! cacheKey) {return {};
   }
 ​
   return {
     onBefore: (params) = > {
       const cacheData = _getCache(cacheKey, params);
 ​
       if(! cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
         return {};
       }
       
       // Data will be assigned regardless of whether the data is fresh or not, but the stale data will continue to be requested (in case of caching).
       // If the data is fresh, stop request
       if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
         return {
           loading: false.data: cacheData? .data,returnNow: true}; }else {
         // If the data is stale, return data, and request continue
         return {
           data: cacheData?.data,
         };
       }
     },
     onRequest: (service, args) = > {
       let servicePromise = cachePromise.getCachePromise(cacheKey);
 ​
       // If has servicePromise, and is not trigger by self, then use it
       if(servicePromise && servicePromise ! == currentPromiseRef.current) {return{ servicePromise }; } servicePromise = service(... args); currentPromiseRef.current = servicePromise;// Cache promises by key (feature 1)
       cachePromise.setCachePromise(cacheKey, servicePromise);
       return { servicePromise };
     },
     onSuccess: (data, params) = > {
       // Only successful data will be cached
       if (cacheKey) {
         // cancel subscribe, avoid trgger selfunSubscribeRef.current? . (); _setCache(cacheKey, { data, params,/ / the cache params
           time: new Date().getTime(),
         });
         // resubscribe
         unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) = > {
           fetchInstance.setState({ data: d }); }); }},// Same as onSuccess when manually modifying data
     onMutate: (data) = > {
       if (cacheKey) {
         // cancel subscribe, avoid trgger selfunSubscribeRef.current? . (); _setCache(cacheKey, { data,params: fetchInstance.state.params,
           time: new Date().getTime(),
         });
         // resubscribe
         unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) = > {
           fetchInstance.setState({ data: d }); }); }}}; };export default useCachePlugin;
Copy the code
  • Let’s look at the implementation of util in the Cache Plugin
 // cache.ts 
 const cache = new Map<CachedKey, RecordData>();
 ​
 // Use a Map to store data and use setTimeout to periodically clear data
 const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) = > {
   const currentCache = cache.get(key);
   // If there is a counter cleared
   if(currentCache? .timer) {clearTimeout(currentCache.timer);
   }
   let timer: Timer | undefined = undefined;
   // If cached data is not never expired
   if (cacheTime > -1) {
     // if cache out, clear it
     timer = setTimeout(() = >{ cache.delete(key); }, cacheTime); } cache.set(key, { ... cachedData, timer,/ / timer
   });
 };
 ​
 const getCache = (key: CachedKey) = > {
   return cache.get(key);
 };
 ​
 // Clear the cache
 const clearCache = (key? :string | string[]) = > {
   if (key) {
     const cacheKeys = Array.isArray(key) ? key : [key];
     cacheKeys.forEach((cacheKey) = > cache.delete(cacheKey));
   } else{ cache.clear(); }};export { getCache, setCache, clearCache };
 ​
 // cachePromise.ts
 const cachePromise = new Map<CachedKey, Promise<any> > ();const getCachePromise = (cacheKey: CachedKey) = > {
   return cachePromise.get(cacheKey);
 };
 ​
 const setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) = > {
   // Should cache the same promise, cannot be promise.finally
   // Because the promise.finally will change the reference of the promise
   cachePromise.set(cacheKey, promise);
 ​
   // No use promise. Finally for compatibility(compatibility)
   // Delete yourself after the request, which also guarantees the request Promise share
   promise
     .then((res) = > {
       cachePromise.delete(cacheKey);
       return res;
     })
     .catch((err) = > {
       cachePromise.delete(cacheKey);
       throw err;
     });
 };
 ​
 export { getCachePromise, setCachePromise };
 ​
 // cacheSubscribe.ts
 // Publish subscribe mode
 const listeners: Record<string, Listener[]> = {};
 ​
 const trigger = (key: string, data: any) = > {
   if (listeners[key]) {
     listeners[key].forEach((item) = >item(data)); }};const subscribe = (key: string, listener: Listener) = > {
   if(! listeners[key]) { listeners[key] = []; } listeners[key].push(listener);return function unsubscribe() {
     const index = listeners[key].indexOf(listener);
     listeners[key].splice(index, 1);
   };
 };
 ​
 export { trigger, subscribe };
Copy the code

3. conclusion

  • That’s all about useRequest, with custom hooks like this we do have a lot less template code in development, and we don’t have to package common functions ourselves.
  • If you want to see how other request libraries work, take a look at my other axios article, which quietly brings Umi-Request