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): add afterFetch option, onFetchResponse, and onFetchError #506

Merged
merged 1 commit into from
May 13, 2021
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
27 changes: 27 additions & 0 deletions packages/core/useFetch/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ const { data } = useFetch(url, {
})
```

The `afterFetch` option can intercept the response data before it is updated.
```ts
const { data } = useFetch(url, {
afterFetch(ctx) {
if (ctx.data.title === 'HxH')
ctx.data.title = 'Hunter x Hunter' // Modifies the resposne data

return ctx
},
})
```

### Setting the request method and return type
The request method and return type can be set by adding the appropriate methods to the end of `useFetch`

Expand Down Expand Up @@ -110,6 +122,21 @@ const useMyFetch = createFetch({
const { isFetching, error, data } = useMyFetch('users')
```

### Events

The `onFetchResposne` and `onFetchError` will fire on fetch request responses and errors respectively.

```ts
const { onFetchResponse, onFetchError } = useFetch(url)

onFetchResponse((response) => {
console.log(response.status)
})

onFetchError((error) => {
console.error(error.message)
})
```
<!--FOOTER_STARTS-->
## Type Declarations

Expand Down
78 changes: 68 additions & 10 deletions packages/core/useFetch/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useFetch, createFetch } from '.'
import fetchMock from 'jest-fetch-mock'
import { when } from '@vueuse/shared'
import { until } from '@vueuse/shared'
import { nextTick, ref } from 'vue-demi'

describe('useFetch', () => {
Expand All @@ -15,7 +15,7 @@ describe('useFetch', () => {

const { data, statusCode, isFinished } = useFetch('https://example.com')

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)

expect(statusCode.value).toBe(200)
expect(data.value).toBe('Hello World')
Expand All @@ -26,7 +26,7 @@ describe('useFetch', () => {

const { data, isFinished } = useFetch('https://example.com').json()

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)

expect(data.value).toStrictEqual({ message: 'Hello World' })
})
Expand All @@ -36,7 +36,7 @@ describe('useFetch', () => {

const { error, statusCode, isFinished } = useFetch('https://example.com')

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)

expect(statusCode.value).toBe(400)
expect(error.value).toBe('Bad Request')
Expand All @@ -49,14 +49,14 @@ describe('useFetch', () => {

setTimeout(() => abort(), 0)

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)
expect(aborted.value).toBe(true)

execute()

setTimeout(() => abort(), 0)

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)
expect(aborted.value).toBe(true)
})

Expand All @@ -75,10 +75,10 @@ describe('useFetch', () => {
const url = ref('https://example.com')
const { isFinished } = useFetch(url, { refetch: true })

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)
url.value = 'https://example.com/test'
await nextTick()
await when(isFinished).toBe(true)
await until(isFinished).toBe(true)

expect(fetchMock).toBeCalledTimes(2)
})
Expand All @@ -89,7 +89,7 @@ describe('useFetch', () => {
const useMyFetch = createFetch({ baseUrl: 'https://example.com', fetchOptions: { headers: { Authorization: 'test' } } })
const { isFinished } = useMyFetch('test', { headers: { 'Accept-Language': 'en-US' } })

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)

expect(fetchMock.mock.calls[0][1]!.headers).toMatchObject({ 'Authorization': 'test', 'Accept-Language': 'en-US' })
expect(fetchMock.mock.calls[0][0]).toEqual('https://example.com/test')
Expand All @@ -109,7 +109,7 @@ describe('useFetch', () => {
},
})

await when(isFinished).toBe(true)
await until(isFinished).toBe(true)

expect(fetchMock.mock.calls[0][1]!.headers).toMatchObject({ 'Authorization': 'my-auth-token', 'Accept-Language': 'en-US' })
})
Expand All @@ -128,4 +128,62 @@ describe('useFetch', () => {

expect(fetchMock).toBeCalledTimes(0)
})

test('should run the afterFetch function', async() => {
fetchMock.mockResponse(JSON.stringify({ title: 'HxH' }), { status: 200 })

const { isFinished, data } = useFetch('https://example.com', {
afterFetch(ctx) {
if (ctx.data.title === 'HxH')
ctx.data.title = 'Hunter x Hunter'

return ctx
},
}).json()

await until(isFinished).toBe(true)
expect(data.value).toStrictEqual({ title: 'Hunter x Hunter' })
})

test('should emit onFetchResponse event', async() => {
let didEventFire = false
const { isFinished, onFetchResponse } = useFetch('https://example.com')

onFetchResponse(() => {
didEventFire = true
})

await until(isFinished).toBe(true)
expect(didEventFire).toBe(true)
})

test('should emit onFetchResponse event', async() => {
fetchMock.mockResponse('', { status: 200 })

let didResponseEventFire = false
let didErrorEventFire = false
const { isFinished, onFetchResponse, onFetchError } = useFetch('https://example.com')

onFetchResponse(() => didResponseEventFire = true)
onFetchError(() => didErrorEventFire = true)

await until(isFinished).toBe(true)
expect(didResponseEventFire).toBe(true)
expect(didErrorEventFire).toBe(false)
})

test('should emit onFetchError event', async() => {
fetchMock.mockResponse('', { status: 400 })

let didResponseEventFire = false
let didErrorEventFire = false
const { isFinished, onFetchResponse, onFetchError } = useFetch('https://example.com')

onFetchResponse(() => didResponseEventFire = true)
onFetchError(() => didErrorEventFire = true)

await until(isFinished).toBe(true)
expect(didResponseEventFire).toBe(false)
expect(didErrorEventFire).toBe(true)
})
})
43 changes: 40 additions & 3 deletions packages/core/useFetch/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Ref, ref, unref, watch, computed, ComputedRef, shallowRef } from 'vue-demi'
import { Fn, MaybeRef, containsProp } from '@vueuse/shared'
import { Fn, MaybeRef, containsProp, createEventHook } from '@vueuse/shared'
import { defaultWindow } from '../_configurable'

interface UseFetchReturnBase<T> {
Expand Down Expand Up @@ -52,6 +52,16 @@ interface UseFetchReturnBase<T> {
* Manually call the fetch
*/
execute: () => Promise<any>

/**
* Fires after the fetch request has finished
*/
onFetchResponse: (fn: (response: Response) => void) => { off: () => void }

/**
* Fires after a fetch request error
*/
onFetchError: (fn: (error: any) => void) => { off: () => void }
}

type DataType = 'text' | 'json' | 'blob' | 'arrayBuffer' | 'formData'
Expand Down Expand Up @@ -91,6 +101,14 @@ export interface BeforeFetchContext {
cancel: Fn
}

export interface AfterFetchContext<T = any> {

response: Response

data: T | null

}

export interface UseFetchOptions {
/**
* Fetch function
Expand All @@ -115,6 +133,12 @@ export interface UseFetchOptions {
* Will run immediately before the fetch request is dispatched
*/
beforeFetch?: (ctx: BeforeFetchContext) => Promise<Partial<BeforeFetchContext> | void> | Partial<BeforeFetchContext> | void

/**
* Will run immediately after the fetch request is returned.
* Runs after any 2xx response
*/
afterFetch?: (ctx: AfterFetchContext) => Promise<Partial<AfterFetchContext>> | Partial<AfterFetchContext>
}

export interface CreateFetchOptions {
Expand All @@ -141,7 +165,7 @@ export interface CreateFetchOptions {
* to include the new options
*/
function isFetchOptions(obj: object): obj is UseFetchOptions {
return containsProp(obj, 'immediate', 'refetch', 'beforeFetch')
return containsProp(obj, 'immediate', 'refetch', 'beforeFetch', 'afterFetch')
}

export function createFetch(config: CreateFetchOptions = {}) {
Expand Down Expand Up @@ -213,6 +237,10 @@ export function useFetch<T>(url: MaybeRef<string>, ...args: any[]): UseFetchRetu
fetch = defaultWindow?.fetch,
} = options

// Event Hooks
const responseEvent = createEventHook<Response>()
const errorEvent = createEventHook<any>()

const isFinished = ref(false)
const isFetching = ref(false)
const aborted = ref(false)
Expand Down Expand Up @@ -292,16 +320,22 @@ export function useFetch<T>(url: MaybeRef<string>, ...args: any[]): UseFetchRetu
response.value = fetchResponse
statusCode.value = fetchResponse.status

await fetchResponse[config.type]().then(text => data.value = text as any)
let responseData = await fetchResponse[config.type]()

// see: https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
if (!fetchResponse.ok)
throw new Error(fetchResponse.statusText)

if (options.afterFetch)
({ data: responseData } = await options.afterFetch({ data: responseData, response: fetchResponse }))

data.value = responseData as any
responseEvent.trigger(fetchResponse)
resolve(fetchResponse)
})
.catch((fetchError) => {
error.value = fetchError.message || fetchError.name
errorEvent.trigger(fetchError)
})
.finally(() => {
isFinished.value = true
Expand Down Expand Up @@ -330,6 +364,9 @@ export function useFetch<T>(url: MaybeRef<string>, ...args: any[]): UseFetchRetu
aborted,
abort,
execute,

onFetchResponse: responseEvent.on,
onFetchError: errorEvent.on,
}

const shell: UseFetchReturn<T> = {
Expand Down