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): introduce updateDataOnError option #3092

Merged
merged 11 commits into from Aug 25, 2023
19 changes: 19 additions & 0 deletions packages/core/useFetch/index.md
Expand Up @@ -126,6 +126,25 @@ const { data } = useFetch(url, {
return ctx
},
})
console.log(data.value) // { title: 'Hunter x Hunter' }
```

You can also disallow `onFetchError` to modifies the response data by passing `false` to `returnDataOnFetchError` option.
```ts
const { data } = useFetch(url, {
returnDataOnFetchError: false,
onFetchError(ctx) {
// ctx.data can be null when 5xx response
if (ctx.data === null)
ctx.data = { title: 'Hunter x Hunter' } // Modifies the response data no longer works

ctx.error = new Error('Custom Error') // Modifies the error

return ctx
},
})

console.log(data.value) // null
```

### Setting the request method and return type
Expand Down
55 changes: 51 additions & 4 deletions packages/core/useFetch/index.test.ts
Expand Up @@ -205,19 +205,22 @@ describe.skipIf(isBelowNode18)('useFetch', () => {
options: {
onFetchError(ctx) {
ctx.error = 'Global'
ctx.data = 'Global'
return ctx
},
},
})
const { error } = useMyFetch('test?status=400&json', {
const { data, error } = useMyFetch('test?status=400&json', {
onFetchError(ctx) {
ctx.error += ' Local'
ctx.data += ' Local'
return ctx
},
}).json()

await retry(() => {
expect(error.value).toEqual('Global Local')
expect(data.value).toEqual('Global Local')
})
})

Expand Down Expand Up @@ -277,22 +280,25 @@ describe.skipIf(isBelowNode18)('useFetch', () => {
options: {
onFetchError(ctx) {
ctx.error = 'Global'
ctx.data = 'Global'
return ctx
},
},
})
const { error } = useMyFetch(
const { error, data } = useMyFetch(
'test?status=400&json',
{ method: 'GET' },
{
onFetchError(ctx) {
ctx.error += ' Local'
ctx.data += ' Local'
return ctx
},
}).json()

await retry(() => {
expect(error.value).toEqual('Global Local')
expect(data.value).toEqual('Global Local')
})
})

Expand Down Expand Up @@ -350,19 +356,22 @@ describe.skipIf(isBelowNode18)('useFetch', () => {
options: {
onFetchError(ctx) {
ctx.error = 'Global'
ctx.data = 'Global'
return ctx
},
},
})
const { error } = useMyFetch('test?status=400&json', {
const { data, error } = useMyFetch('test?status=400&json', {
onFetchError(ctx) {
ctx.error = 'Local'
ctx.data = 'Local'
return ctx
},
}).json()

await retry(() => {
expect(error.value).toEqual('Local')
expect(data.value).toEqual('Local')
})
})

Expand Down Expand Up @@ -426,22 +435,25 @@ describe.skipIf(isBelowNode18)('useFetch', () => {
options: {
onFetchError(ctx) {
ctx.error = 'Global'
ctx.data = 'Global'
return ctx
},
},
})
const { error } = useMyFetch(
const { data, error } = useMyFetch(
'test?status=400&json',
{ method: 'GET' },
{
onFetchError(ctx) {
ctx.error = 'Local'
ctx.data = 'Local'
return ctx
},
}).json()

await retry(() => {
expect(error.value).toEqual('Local')
expect(data.value).toEqual('Local')
})
})

Expand Down Expand Up @@ -542,6 +554,24 @@ describe.skipIf(isBelowNode18)('useFetch', () => {
const { data, error, statusCode } = useFetch('https://example.com?status=400&json', {
onFetchError(ctx) {
ctx.error = 'Internal Server Error'
ctx.data = 'Internal Server Error'
return ctx
},
}).json()

await retry(() => {
expect(statusCode.value).toEqual(400)
expect(error.value).toEqual('Internal Server Error')
expect(data.value).toEqual('Internal Server Error')
})
})

it('should not return data in onFetchError when returnDataOnFetchError is false', async () => {
const { data, error, statusCode } = useFetch('https://example.com?status=400&json', {
returnDataOnFetchError: false,
onFetchError(ctx) {
ctx.error = 'Internal Server Error'
ctx.data = 'Internal Server Error'
return ctx
},
}).json()
Expand All @@ -557,7 +587,24 @@ describe.skipIf(isBelowNode18)('useFetch', () => {
const { data, error, statusCode } = useFetch('https://example.com?status=500&text=Internal%20Server%20Error', {
onFetchError(ctx) {
ctx.error = 'Internal Server Error'
ctx.data = 'Internal Server Error'
return ctx
},
}).json()

await retry(() => {
expect(statusCode.value).toStrictEqual(500)
expect(error.value).toEqual('Internal Server Error')
expect(data.value).toEqual('Internal Server Error')
})
})

it('should not return data in onFetchError when returnDataOnFetchError is false and network error', async () => {
const { data, error, statusCode } = useFetch('https://example.com?status=500&text=Internal%20Server%20Error', {
returnDataOnFetchError: false,
onFetchError(ctx) {
ctx.error = 'Internal Server Error'
ctx.data = 'Internal Server Error'
return ctx
},
}).json()
Expand Down
14 changes: 11 additions & 3 deletions packages/core/useFetch/index.ts
Expand Up @@ -163,6 +163,12 @@ export interface UseFetchOptions {
*/
timeout?: number

/**
* Allow `onFetchError` hook to return data
* @default true
*/
returnDataOnFetchError?: boolean

/**
* Will run immediately before the fetch request is dispatched
*/
Expand Down Expand Up @@ -211,7 +217,7 @@ export interface CreateFetchOptions {
* to include the new options
*/
function isFetchOptions(obj: object): obj is UseFetchOptions {
return obj && containsProp(obj, 'immediate', 'refetch', 'initialData', 'timeout', 'beforeFetch', 'afterFetch', 'onFetchError', 'fetch')
return obj && containsProp(obj, 'immediate', 'refetch', 'initialData', 'timeout', 'beforeFetch', 'afterFetch', 'onFetchError', 'fetch', 'returnDataOnFetchError')
}

// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
Expand Down Expand Up @@ -314,7 +320,7 @@ export function useFetch<T>(url: MaybeRefOrGetter<string>, ...args: any[]): UseF
const supportsAbort = typeof AbortController === 'function'

let fetchOptions: RequestInit = {}
let options: UseFetchOptions = { immediate: true, refetch: false, timeout: 0 }
let options: UseFetchOptions = { immediate: true, refetch: false, timeout: 0, returnDataOnFetchError: true }
interface InternalConfig { method: HttpMethod; type: DataType; payload: unknown; payloadType?: string }
const config: InternalConfig = {
method: 'GET',
Expand Down Expand Up @@ -465,8 +471,10 @@ export function useFetch<T>(url: MaybeRefOrGetter<string>, ...args: any[]): UseF
let errorData = fetchError.message || fetchError.name

if (options.onFetchError)
({ error: errorData } = await options.onFetchError({ data: responseData, error: fetchError, response: response.value }))
({ error: errorData, data: responseData } = await options.onFetchError({ data: responseData, error: fetchError, response: response.value }))
error.value = errorData
if (options.returnDataOnFetchError)
data.value = responseData

errorEvent.trigger(fetchError)
if (throwOnFailed)
Expand Down