🌍 background

In the back-end separation project, many of our scenarios required asynchronous requests to get data, and there was a lot of processing involved. For example, loading requests, error capture, and shaking prevention.

In the development process, it is often necessary to write a lot of repetitive code to realize these logic processing. Recently, VUe3 has set off a wave in China, and it is also compatible with TS. Since I am converted from React to Vue, the callback problem when USING AXIos of Vue makes me feel inelegant and inadaptable. This article explains how to package a simple vue3 asynchronous request hook like ahooks/useRequest to meet our needs for these scenarios. Also let everyone intuitive, easy to understand this process.

🌟 function

  • Manual request/automatic request/re-request
  • Conditional/dependent requests
  • Polling/anti-shake/throttling
  • Request data cache/data preservation
  • Format data/change data immediately
  • Callbacks between states
  • Elegant responsive data

use

Const {data, run, loading} = useAsync(here we receive a promise asynchronous requestor,{... Configuration items})Copy the code

UseAsync is an asynchronous request with a promise as the first parameter. It can be a request with a parameter ()=> Request (params) or a request to be passed. Run (params) is required. See ahooks/useRequest for instructions

I. Result term

parameter instructions type
data Data returned by service TData
error Exception thrown by service Error
loading Whether service is being executed boolean
params The array of parameters of the service to be executed this time TParams
run When the service is manually triggered, parameters are passed to the service for automatic handling of exceptionsonErrorfeedback (... params: TParams) => void
refresh Using the last params, call againrun () => void
mutate Directly modifyingdata (data? : TData / ((oldData? : TData) => (TData / undefined))) => void
cancel Cancel a request that is currently in progress () => void

2. Configuration Items Options (Basic Functions)

parameter instructions type The default value
manual The defaultfalse. That is, the service is automatically executed during initialization. If set totrue, you need to manually invoke the callrunTrigger execution. boolean false
defaultParams The parameters passed to the service when first executed by default TParams
onBefore Service is triggered before execution (params: TParams) => void
onSuccess Trigger when service Resolve is triggered (data: TData, params: TParams) => void
onError Service Reject is triggered (e: Error, params: TParams) => void
onFinally Triggered when the service execution is complete (params: TParams, data? : TData, e? : Error) => void

More advanced features ↓

📡 Basic implementation process

Vue3 reactive data declaration. As we know from the result item, we should declare a reactive data for any result that is not a function type. Ref is used for simple data types, and Reactive is used for complex data types such as objects.

First, we need to have these basic data

  const params = ref<TParams>(defaultParams as TParams) as Ref<TParams>;
  const lastSuccessParams = ref<TParams>(defaultParams as TParams) as Ref<TParams>;
  const state: StateType<TData> = reactive({
    data: initialData || null,
    error: null,
    loading: false,
  });
Copy the code

Second, we get the service that is passed in. The service runs around run

const run = async (... args: TParams) => { // ... Const result = await service(... args); // Because reactive deconstruction is unresponsive, only a single value can be assigned to state.data = result; state.loading = false; } catch (err: Error) { onError(err); state.error = err; state.loading = false; }};Copy the code

The basic process has been completed

export function useAsync<TParams = any, TParams extends any[] = any>( service: Service<TData, TParams>, options: OptionsType<TData, TParams> = {} ){ const state: StateType<TData> = reactive({ data: initialData || null, error: null, loading: false, }); const run = async (... args: TParams) => { // ... Const result = await service(... args); // Because reactive deconstruction is unresponsive, only a single value can be assigned to state.data = result; state.loading = false; } catch (err: Error) { onError(err); state.error = err; state.loading = false; }}; return { ... toRefs(state), run } }Copy the code

This code completes the basic asynchronous request hooks in VUE3. State is defined. When the asynchronous request is started and run is entered, the state will be updated when the asynchronous request status changes, and the state will be updated in time when there are errors and exceptions. See ↓ for expanded functions

☄️ useAsync extension

  • Manual Automatic execution

    Locate Mounted. If the value of manual is true, the request is sent. Manual can be passed to a REF here so that watchEffect can be used for dependency collection listening, which I think is also a good solution.

onMounted(() => { if (! manual) run(... (defaultParams as TParams)); });Copy the code
  • Data formatting

    FormatResult changes the data before the onSuccess callback. FormatResult returns data as the parameter callback, and returns the modified value from the outside. The onSuccess callback changes the value and the result item

    state.data = formatResult? .(state.data); onSuccess? .(state.data);Copy the code
  • Image stabilization

    Create an anti-shake function as the middleware of run, need to go through anti-shake processing before run. Debounce belongs to Lodash

    ServiceRun is a run to be processed

    let serviceRun = run; if (debounceInterval) { const debounceRun = debounce(run, debounceInterval); serviceRun = (... args: TParams) => { state.loading = true; return Promise.resolve(debounceRun(... args)!) ; }; }Copy the code
  • The throttle

    Create a throttling function as middleware for run, which needs to be throttled before running. Throttling Debounce belongs to Lodash

    ServiceRun ditto

    if (throttleInterval) { const throttleRun = throttle(run, throttleInterval); serviceRun = (... args: TParams) => { return Promise.resolve(throttleRun(... args)!) ; }; }Copy the code
  • polling

    Create a polling function as the middleware of run, which needs polling before running.

    ServiceRun ditto

    let pollingTimer: Timer | undefined; if (pollingInterval) { serviceRun = (... args: P) => { if (pollingTimer) { clearInterval(pollingTimer); } pollingTimer = setInterval (() = > {/ / here you can add the polling not processing logic, such as the page to hide don't send when polling, can define the field processing run (... args); }, pollingInterval); return run(... args); }; }Copy the code
  • Cancel the request

    The loading state changes because the logic such as the timer is cancelled.

    function cancel = () => { if (pollingTimer) { clearInterval(pollingTimer); } state.loading = false; }; // We can define a variable count to count the current number of requests, count the current number of requests before the run request and assign a currentCount value, equal to the currentCount value. If you cancel the request, you can increase count by 1 to make the count inconsistent during the asynchronous requestCopy the code
  • To request

    Resend the request with params from the previous request

    function refresh() { return run(... params.value); }Copy the code
  • Change data immediately – mutation

    Provides functions to change data from any external location

      function mutate(data: TData) {
        state.data = data;
      }
    Copy the code
  • Conditions of the request

    Ready passes in a ref response and sends the request when true

      watch(ready, (val) => {
        if (val === true) {
          run(...params.value);
        }
      });
    Copy the code
  • Rely on request

    RefreshDeps is an array of ref, and Watch monitors the change of dependency. If it changes, watch will look for whether it is a params dependency. If it is, the request will be made by replacing the value of the attribute in the params.

    Watch (refreshDeps, (value, prevValue) => {params.value = (params.value as any[])? .map((item) => { const obj = item; (prevValue as any[]).forEach((v, index) => { Object.keys(item).forEach((key) => { if (item[key] === v) { obj[key] = value[index]; }}); }); return { params.value, ... obj, }; }) as TParams; run(... params.value); });Copy the code
  • Loading delay

    Loading Delay prevents data flickering

    LoadingDelay if (loadingDelay! == undefined) { timerRef.value = setTimeout(() => { state.loading = true; }, loadingDelay); state.loading = false; }Copy the code
  • Request data caching

    It is used with the cacheKey, which caches requested data, prioritises cached data for the next request, and secretly sends data behind the scenes to update the cache.

    You can specify cacheTime to set the cache expiration time, which will be cleared.

    const run = async (... Args: TParams) => {// run immediately before using cache if (cacheKey) {state.data = getCache(cacheKey)? .data; } / /... If (cacheKey) {setCache(cacheKey, cacheTime, state.data, cloneDeep(args)); } onSuccess? .(state.data); }Copy the code

    Cache. Ts file

    type Timer = ReturnType<typeof setTimeout>; type CachedKey = string | number; type CachedData = { data: any; params: any; timer: Timer | undefined; time: number; }; type Listener = (data: any) => void; Const cache = new Map<CachedKey, CachedData>(); const listeners: Record<string, Listener[]> = {}; // setCache const setCache = (key: CachedKey, cacheTime: number, data: any, params: any ) => { const currentCache = cache.get(key); If (currentCache? .timer) { clearTimeout(currentCache.timer); } let timer: Timer | undefined = undefined; If (cacheTime > -1) {timer = setTimeout(() => {cache.delete(key); }, cacheTime); } if (listeners[key]) { listeners[key].forEach((item) => item(data)); } cache.set(key, { data, params, timer, time: new Date().getTime(), }); }; const getCache = (key: CachedKey) => { return cache.get(key); }; 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); }; }; // 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, subscribe, clearCache };Copy the code
  • Parallel requests

    FetchKey is required

    Fetches are state-like collections that store the state of the same request (mainly the collection of states with inconsistent parameters for the same request)

    const fetches = reactive< Record< string, StateType<any> & { params: any; } > > ({}); // Take the first parameter, you can improve it. Const fetchKeyPersist = fetchKey({... args }? . [0]???? "default_key"); / /... If (fetchKeyPersist) {fetches[fetchKeyPersist as string] = {... state, loading: true, params: cloneDeep(args), }; } if (fetchKeyPersist) {fetches[fetchKeyPersist as string] = {... state, loading: false, params: { ... args }, }; } if (fetchKeyPersist) {fetches[fetchKeyPersist as string] = {... state, data: null, params: cloneDeep(args), }; }Copy the code

The above are all advanced extended functions, which are interspersed with each life node to execute corresponding logic just like a hook.

💫 attached source code

Language finches useAsync

👬 Vue’s good buddy Axios

import axios, { AxiosRequestConfig } from "axios"; import { Toast } from "vant"; import { routers } from ".. /routers"; / / post request head axios. Defaults. Headers. Post [] "the content-type" = "application/x - WWW - form - urlencoded; charset=UTF-8"; // axios.defaults.timeout = 10_000; const axiosInstance = axios.create({ timeout: 10000, }); axiosInstance.interceptors.request.use( (config) => { const accessToken = sessionStorage.getItem("access_token"); if (accessToken) { return { ... config, headers: { ... config.headers, Authorization: accessToken ? `Bearer ${accessToken}` : "", }, }; } return config; }, (error) => { return Promise.reject(error); }); axiosInstance.interceptors.response.use( (response) => { if (response?.status === 200) { return Promise.resolve(response); } else { return Promise.reject(response); } }, (error) => { if (error? .message? .includes? .("timeout")) {toast.fail (" request timeout"); } else {toast.fail (" Network error, please try again "); routers.push("/403"); } Promise.reject(error); }); const request = <ResponseType = unknown>( url: string, options? : AxiosRequestConfig<unknown> ): Promise<ResponseType> => { return new Promise((resolve, reject) => { axiosInstance({ url, ... options, }) .then((res) => { resolve(res.data.data); }) .catch((err) => reject(err)); }); }; export { axiosInstance, request };Copy the code

🌰 use

  • Return a promise request exported from ⬆️

  • For non-mandatory display function only, according to their own needs to delete and add logic

👩 🏻 💻 summary

This hook can help us save a lot of repeated logic, and the difficulty is not high, suitable for those who need elegant request and use for learning, currently there is no problem to use in the project. We can also add and improve on our own scenarios. In general, it borrows from ahooks.

Insufficient 🌚

This is a simple version of request hook. Although it has many simple functions, it also has obvious drawbacks. Undeniably, it can fulfill most of our scene requirements. But it still works for most scenarios, so it won’t change much.

🤩 outlook

UseAsync provides plug-in logic function expansion, completely solve the code chaos, easy to manage. There is a separate instance for each request instance. We need to expand the function, we just need to write according to the specification and reasonably use the callback and examples exposed by hook. Powerful plug-in type asynchronous management tool 🔧 development…