Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useFetch): allow configure lifecycle handler behavior #2333

Merged
merged 6 commits into from Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/core/useFetch/index.md
Expand Up @@ -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.
Expand Down
155 changes: 153 additions & 2 deletions 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))}`
Expand Down Expand Up @@ -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 }) {
Expand Down
48 changes: 34 additions & 14 deletions 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'

Expand Down Expand Up @@ -90,6 +90,7 @@ export interface UseFetchReturn<T> {

type DataType = 'text' | 'json' | 'blob' | 'arrayBuffer' | 'formData'
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
type Combination = 'overwrite' | 'chain'

const payloadMapping: Record<string, string> = {
json: 'application/json',
Expand Down Expand Up @@ -187,6 +188,12 @@ export interface CreateFetchOptions {
*/
baseUrl?: MaybeComputedRef<string>

/**
* Determine the inherit behavior for beforeFetch, afterFetch, onFetchError
* @default 'chain'
*/
combination?: Combination
wheatjs marked this conversation as resolved.
Show resolved Hide resolved

/**
* Default Options for the useFetch function
*/
Expand Down Expand Up @@ -214,17 +221,30 @@ function headersToObject(headers: HeadersInit | undefined) {
return headers
}

function chainCallbacks<T = any>(...callbacks: (((ctx: T) => void | Partial<T> | Promise<void | Partial<T>>) | 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<T = any>(combination: Combination, ...callbacks: (((ctx: T) => void | Partial<T> | Promise<void | Partial<T>>) | 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 || {}

Expand All @@ -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 {
Expand All @@ -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),
}
}

Expand Down