From 438bfaf80403dce04fcf5a5262b77fd2ed63a4d5 Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 18 Oct 2022 15:33:25 +0800 Subject: [PATCH 1/6] feat(useFetch): allow configurate lifecycle handler behavior --- packages/core/useFetch/index.md | 40 +++++++ packages/core/useFetch/index.test.ts | 155 ++++++++++++++++++++++++++- packages/core/useFetch/index.ts | 52 ++++++--- 3 files changed, 228 insertions(+), 19 deletions(-) diff --git a/packages/core/useFetch/index.md b/packages/core/useFetch/index.md index b227849ad06..714a8a662c0 100644 --- a/packages/core/useFetch/index.md +++ b/packages/core/useFetch/index.md @@ -166,6 +166,46 @@ const useMyFetch = createFetch({ const { isFetching, error, data } = useMyFetch('users') ``` +If you want to control the behavior of `beforeFetch`, `afterFetch`, `onFetchError` between the pre-configured instance and newly spawned instance. You can provide a `combination` option to toggle between `overwrite` or `chaining`. + +```ts +const useMyFetch = createFetch({ + baseUrl: 'https://my-api.com', + combination: 'overwrite', + options: { + // beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch + async beforeFetch({ options }) { + const myToken = await getMyToken() + options.headers.Authorization = `Bearer ${myToken}` + + return { options } + }, + }, +}) + +// use useMyFetch beforeFetch +const { isFetching, error, data } = useMyFetch('users') + +// use custom beforeFetch +const { isFetching, error, data } = useMyFetch('users', { + async beforeFetch({ url, options, cancel }) { + const myToken = await getMyToken() + + if (!myToken) + cancel() + + options.headers = { + ...options.headers, + Authorization: `Bearer ${myToken}`, + } + + return { + options, + } + }, +}) +``` + ### Events The `onFetchResponse` and `onFetchError` will fire on fetch request responses and errors respectively. diff --git a/packages/core/useFetch/index.test.ts b/packages/core/useFetch/index.test.ts index 0ce62112940..7163140ebaa 100644 --- a/packages/core/useFetch/index.test.ts +++ b/packages/core/useFetch/index.test.ts @@ -1,8 +1,8 @@ -import { ref } from 'vue-demi' import { until } from '@vueuse/shared' +import { ref } from 'vue-demi' +import { createFetch, useFetch } from '.' import { retry } from '../../.test' import '../../.test/mockServer' -import { createFetch, useFetch } from '.' const jsonMessage = { hello: 'world' } const jsonUrl = `https://example.com?json=${encodeURI(JSON.stringify(jsonMessage))}` @@ -286,6 +286,157 @@ describe('useFetch', () => { }) }) + test('should overwrite beforeFetch function when using a factory instance', async () => { + const useMyFetch = createFetch({ + baseUrl: 'https://example.com', + combination: 'overwrite', + options: { + beforeFetch({ options }) { + options.headers = { ...options.headers, Global: 'foo' } + return { options } + }, + }, + }) + useMyFetch('test', { + beforeFetch({ options }) { + options.headers = { ...options.headers, Local: 'foo' } + return { options } + }, + }) + + await retry(() => { + expect(fetchSpyHeaders()).toMatchObject({ Local: 'foo' }) + }) + }) + + test('should overwrite afterFetch function when using a factory instance', async () => { + const useMyFetch = createFetch({ + baseUrl: 'https://example.com', + combination: 'overwrite', + options: { + afterFetch(ctx) { + ctx.data.global = 'Global' + return ctx + }, + }, + }) + const { data } = useMyFetch('test?json', { + afterFetch(ctx) { + ctx.data.local = 'Local' + return ctx + }, + }).json() + + await retry(() => { + expect(data.value).toEqual(expect.objectContaining({ local: 'Local' })) + expect(data.value).toEqual(expect.not.objectContaining({ global: 'Global' })) + }) + }) + + test('should overwrite onFetchError function when using a factory instance', async () => { + const useMyFetch = createFetch({ + baseUrl: 'https://example.com', + combination: 'overwrite', + options: { + onFetchError(ctx) { + ctx.data.global = 'Global' + return ctx + }, + }, + }) + const { data } = useMyFetch('test?status=400&json', { + onFetchError(ctx) { + ctx.data.local = 'Local' + return ctx + }, + }).json() + + await retry(() => { + expect(data.value).toEqual(expect.objectContaining({ local: 'Local' })) + expect(data.value).toEqual(expect.not.objectContaining({ global: 'Global' })) + }) + }) + + test('should overwrite beforeFetch function when using a factory instance and the options object in useMyFetch', async () => { + const useMyFetch = createFetch({ + baseUrl: 'https://example.com', + combination: 'overwrite', + options: { + beforeFetch({ options }) { + options.headers = { ...options.headers, Global: 'foo' } + return { options } + }, + }, + }) + useMyFetch( + 'test', + { method: 'GET' }, + { + beforeFetch({ options }) { + options.headers = { ...options.headers, Local: 'foo' } + return { options } + }, + }) + + await retry(() => { + expect(fetchSpyHeaders()).toMatchObject({ Local: 'foo' }) + }) + }) + + test('should overwrite afterFetch function when using a factory instance and the options object in useMyFetch', async () => { + const useMyFetch = createFetch({ + baseUrl: 'https://example.com', + combination: 'overwrite', + options: { + afterFetch(ctx) { + ctx.data.global = 'Global' + return ctx + }, + }, + }) + const { data } = useMyFetch( + 'test?json', + { method: 'GET' }, + { + afterFetch(ctx) { + ctx.data.local = 'Local' + return ctx + }, + }).json() + + await retry(() => { + expect(data.value).toEqual(expect.objectContaining({ local: 'Local' })) + expect(data.value).toEqual(expect.not.objectContaining({ global: 'Global' })) + }) + }) + + test('should overwrite onFetchError function when using a factory instance and the options object in useMyFetch', async () => { + const useMyFetch = createFetch({ + baseUrl: 'https://example.com', + combination: 'overwrite', + options: { + onFetchError(ctx) { + ctx.data.global = 'Global' + return ctx + }, + }, + }) + const { data } = useMyFetch( + 'test?status=400&json', + { method: 'GET' }, + { + onFetchError(ctx) { + ctx.data.local = 'Local' + return ctx + }, + }).json() + + await retry(() => { + expect(data.value).toEqual(expect.objectContaining({ local: 'Local' })) + expect(data.value).toEqual(expect.not.objectContaining({ global: 'Global' })) + }) + }) + test('should run the beforeFetch function and add headers to the request', async () => { useFetch('https://example.com', { headers: { 'Accept-Language': 'en-US' } }, { beforeFetch({ options }) { diff --git a/packages/core/useFetch/index.ts b/packages/core/useFetch/index.ts index 3d1f701630e..0e99847a43c 100644 --- a/packages/core/useFetch/index.ts +++ b/packages/core/useFetch/index.ts @@ -1,6 +1,6 @@ -import type { ComputedRef, Ref } from 'vue-demi' import type { EventHookOn, Fn, MaybeComputedRef, Stoppable } from '@vueuse/shared' import { containsProp, createEventHook, resolveRef, resolveUnref, until, useTimeoutFn } from '@vueuse/shared' +import type { ComputedRef, Ref } from 'vue-demi' import { computed, isRef, ref, shallowRef, watch } from 'vue-demi' import { defaultWindow } from '../_configurable' @@ -90,6 +90,7 @@ export interface UseFetchReturn { type DataType = 'text' | 'json' | 'blob' | 'arrayBuffer' | 'formData' type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' +type Combination = 'overwrite' | 'combineCallbacksin' const payloadMapping: Record = { json: 'application/json', @@ -141,8 +142,8 @@ export interface UseFetchOptions { /** * Will automatically refetch when: - * - the URL is changed if the URL is a ref - * - the payload is changed if the payload is a ref + * - the URL is combineCallbacksnged if the URL is a ref + * - the payload is combineCallbacksnged if the payload is a ref * * @default false */ @@ -187,6 +188,12 @@ export interface CreateFetchOptions { */ baseUrl?: MaybeComputedRef + /** + * Determine the inherit behavior for beforeFetch, afterFetch, onFetchError + * @default 'combineCallbacksin' + */ + combination?: Combination + /** * Default Options for the useFetch function */ @@ -214,17 +221,28 @@ function headersToObject(headers: HeadersInit | undefined) { return headers } -function chainCallbacks(...callbacks: (((ctx: T) => void | Partial | Promise>) | undefined)[]) { - return async (ctx: T) => { - await callbacks.reduce((prevCallback, callback) => prevCallback.then(async () => { - if (callback) - ctx = { ...ctx, ...(await callback(ctx)) } - }), Promise.resolve()) - return ctx +function combineCallbacks(combination: Combination, ...callbacks: (((ctx: T) => void | Partial | Promise>) | undefined)[]) { + if(combination === 'overwrite') { + // use last callback + return async (ctx: T) => { + const callback = callbacks.at(callbacks.length - 1) + if(callback !== undefined) await callback(ctx) + return ctx + } + } else { + // chaining and combine result + return async (ctx: T) => { + await callbacks.reduce((prevCallback, callback) => prevCallback.then(async () => { + if (callback) + ctx = { ...ctx, ...(await callback(ctx)) } + }), Promise.resolve()) + return ctx + } } } export function createFetch(config: CreateFetchOptions = {}) { + const _combination = config.combination || 'chain' const _options = config.options || {} const _fetchOptions = config.fetchOptions || {} @@ -243,9 +261,9 @@ export function createFetch(config: CreateFetchOptions = {}) { options = { ...options, ...args[0], - beforeFetch: chainCallbacks(_options.beforeFetch, args[0].beforeFetch), - afterFetch: chainCallbacks(_options.afterFetch, args[0].afterFetch), - onFetchError: chainCallbacks(_options.onFetchError, args[0].onFetchError), + beforeFetch: combineCallbacks(_combination, _options.beforeFetch, args[0].beforeFetch), + afterFetch: combineCallbacks(_combination, _options.afterFetch, args[0].afterFetch), + onFetchError: combineCallbacks(_combination, _options.onFetchError, args[0].onFetchError), } } else { @@ -264,9 +282,9 @@ export function createFetch(config: CreateFetchOptions = {}) { options = { ...options, ...args[1], - beforeFetch: chainCallbacks(_options.beforeFetch, args[1].beforeFetch), - afterFetch: chainCallbacks(_options.afterFetch, args[1].afterFetch), - onFetchError: chainCallbacks(_options.onFetchError, args[1].onFetchError), + beforeFetch: combineCallbacks(_combination, _options.beforeFetch, args[1].beforeFetch), + afterFetch: combineCallbacks(_combination, _options.afterFetch, args[1].afterFetch), + onFetchError: combineCallbacks(_combination, _options.onFetchError, args[1].onFetchError), } } @@ -490,7 +508,7 @@ export function useFetch(url: MaybeComputedRef, ...args: any[]): UseF config.payload = payload config.payloadType = payloadType - // watch for payload changes + // watch for payload combineCallbacksnges if (isRef(config.payload)) { watch( [ From f11621b8644fc12516b293ef49cefd714078561d Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 18 Oct 2022 15:37:27 +0800 Subject: [PATCH 2/6] fix: types mismatch --- packages/core/useFetch/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useFetch/index.ts b/packages/core/useFetch/index.ts index 0e99847a43c..d0f0de94a20 100644 --- a/packages/core/useFetch/index.ts +++ b/packages/core/useFetch/index.ts @@ -242,7 +242,7 @@ function combineCallbacks(combination: Combination, ...callbacks: (((ct } export function createFetch(config: CreateFetchOptions = {}) { - const _combination = config.combination || 'chain' + const _combination = config.combination || 'chain' as Combination const _options = config.options || {} const _fetchOptions = config.fetchOptions || {} From 49429e68252927b339f7bf0228568d6677c0697b Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 18 Oct 2022 15:39:12 +0800 Subject: [PATCH 3/6] style: linting --- packages/core/useFetch/index.test.ts | 2 +- packages/core/useFetch/index.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/useFetch/index.test.ts b/packages/core/useFetch/index.test.ts index 7163140ebaa..920b8b6f5fb 100644 --- a/packages/core/useFetch/index.test.ts +++ b/packages/core/useFetch/index.test.ts @@ -1,7 +1,7 @@ import { until } from '@vueuse/shared' import { ref } from 'vue-demi' -import { createFetch, useFetch } from '.' import { retry } from '../../.test' +import { createFetch, useFetch } from '.' import '../../.test/mockServer' const jsonMessage = { hello: 'world' } diff --git a/packages/core/useFetch/index.ts b/packages/core/useFetch/index.ts index d0f0de94a20..31c77ef8b9a 100644 --- a/packages/core/useFetch/index.ts +++ b/packages/core/useFetch/index.ts @@ -222,14 +222,16 @@ function headersToObject(headers: HeadersInit | undefined) { } function combineCallbacks(combination: Combination, ...callbacks: (((ctx: T) => void | Partial | Promise>) | undefined)[]) { - if(combination === 'overwrite') { + if (combination === 'overwrite') { // use last callback return async (ctx: T) => { const callback = callbacks.at(callbacks.length - 1) - if(callback !== undefined) await callback(ctx) + if (callback !== undefined) + await callback(ctx) return ctx } - } else { + } + else { // chaining and combine result return async (ctx: T) => { await callbacks.reduce((prevCallback, callback) => prevCallback.then(async () => { From 4b31e633c1dd55554f993736e2efbb985a576d06 Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 18 Oct 2022 18:00:14 +0800 Subject: [PATCH 4/6] fix: node@14 syntax error --- packages/core/useFetch/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useFetch/index.ts b/packages/core/useFetch/index.ts index 31c77ef8b9a..5455f0b7dd7 100644 --- a/packages/core/useFetch/index.ts +++ b/packages/core/useFetch/index.ts @@ -225,7 +225,7 @@ function combineCallbacks(combination: Combination, ...callbacks: (((ct if (combination === 'overwrite') { // use last callback return async (ctx: T) => { - const callback = callbacks.at(callbacks.length - 1) + const callback = callbacks[callbacks.length - 1] if (callback !== undefined) await callback(ctx) return ctx From ae51a78e5ec76f5b59fca55ecf88764a340a1ff9 Mon Sep 17 00:00:00 2001 From: KaKa Date: Wed, 26 Oct 2022 23:53:59 +0800 Subject: [PATCH 5/6] fix: typo --- packages/core/useFetch/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/useFetch/index.ts b/packages/core/useFetch/index.ts index 5455f0b7dd7..54ba00ff7ed 100644 --- a/packages/core/useFetch/index.ts +++ b/packages/core/useFetch/index.ts @@ -90,7 +90,7 @@ export interface UseFetchReturn { type DataType = 'text' | 'json' | 'blob' | 'arrayBuffer' | 'formData' type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' -type Combination = 'overwrite' | 'combineCallbacksin' +type Combination = 'overwrite' | 'chain' const payloadMapping: Record = { json: 'application/json', @@ -142,8 +142,8 @@ export interface UseFetchOptions { /** * Will automatically refetch when: - * - the URL is combineCallbacksnged if the URL is a ref - * - the payload is combineCallbacksnged if the payload is a ref + * - the URL is changed if the URL is a ref + * - the payload is changed if the payload is a ref * * @default false */ @@ -190,7 +190,7 @@ export interface CreateFetchOptions { /** * Determine the inherit behavior for beforeFetch, afterFetch, onFetchError - * @default 'combineCallbacksin' + * @default 'chain' */ combination?: Combination From adab9bbe7e27ea4f33fe599738e31d7eb125cb3a Mon Sep 17 00:00:00 2001 From: KaKa Date: Thu, 27 Oct 2022 13:28:22 +0800 Subject: [PATCH 6/6] chore: typo --- packages/core/useFetch/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useFetch/index.ts b/packages/core/useFetch/index.ts index 54ba00ff7ed..5cabeef2fbb 100644 --- a/packages/core/useFetch/index.ts +++ b/packages/core/useFetch/index.ts @@ -510,7 +510,7 @@ export function useFetch(url: MaybeComputedRef, ...args: any[]): UseF config.payload = payload config.payloadType = payloadType - // watch for payload combineCallbacksnges + // watch for payload changes if (isRef(config.payload)) { watch( [