🌍 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 exceptionsonError feedback |
(... 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 callrun Trigger 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…