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..920b8b6f5fb 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 { retry } from '../../.test' -import '../../.test/mockServer' import { createFetch, useFetch } from '.' +import '../../.test/mockServer' 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..5cabeef2fbb 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' | 'chain' const payloadMapping: Record = { json: 'application/json', @@ -187,6 +188,12 @@ export interface CreateFetchOptions { */ baseUrl?: MaybeComputedRef + /** + * Determine the inherit behavior for beforeFetch, afterFetch, onFetchError + * @default 'chain' + */ + combination?: Combination + /** * Default Options for the useFetch function */ @@ -214,17 +221,30 @@ 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[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' as Combination const _options = config.options || {} const _fetchOptions = config.fetchOptions || {} @@ -243,9 +263,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 +284,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), } }