Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,7 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr
| `interceptors.response` | Allows you to do something after an http response is recieved. Useful for something like camelCasing the keys of the response. | `undefined` |
| `loading` | Allows you to set default value for `loading` | `false` unless the last argument of `useFetch` is `[]` |
| `onAbort` | Runs when the request is aborted. | empty function |
| `onError` | Runs when the request get's an error. If retrying, it is only called on the last retry attempt. | empty function |
| `onNewData` | Merges the current data with the incoming data. Great for pagination. | `(curr, new) => new` |
| `onTimeout` | Called when the request times out. | empty function |
| `path` | When using a global `url` set in the `Provider`, this is useful for adding onto it | `''` |
Expand Down Expand Up @@ -894,6 +895,9 @@ const options = {
// called when aborting the request
onAbort: () => {},

// runs when an error happens.
onError: ({ error }) => {},

// this will allow you to merge the `data` for pagination.
onNewData: (currData, newData) => {
return [...currData, ...newData]
Expand Down Expand Up @@ -1062,12 +1066,10 @@ Todos
// potential idea to fetch on server instead of just having `loading` state. Not sure if this is a good idea though
onServer: true,
onSuccess: (/* idk what to put here */) => {},
onError: (error) => {},
// if you would prefer to pass the query in the config
query: `some graphql query`
// if you would prefer to pass the mutation in the config
mutation: `some graphql mutation`
retryOnError: false,
refreshWhenHidden: false,
})

Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr
| `interceptors.response` | Allows you to do something after an http response is recieved. Useful for something like camelCasing the keys of the response. | `undefined` |
| `loading` | Allows you to set default value for `loading` | `false` unless the last argument of `useFetch` is `[]` |
| `onAbort` | Runs when the request is aborted. | empty function |
| `onError` | Runs when the request get's an error. If retrying, it is only called on the last retry attempt. | empty function |
| `onNewData` | Merges the current data with the incoming data. Great for pagination. | `(curr, new) => new` |
| `onTimeout` | Called when the request times out. | empty function |
| `path` | When using a global `url` set in the `Provider`, this is useful for adding onto it | `''` |
Expand Down Expand Up @@ -845,6 +846,9 @@ const options = {
// called when aborting the request
onAbort: () => {},

// runs when an error happens.
onError: ({ error }) => {},

// this will allow you to merge the `data` for pagination.
onNewData: (currData, newData) => {
return [...currData, ...newData]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "use-http",
"version": "1.0.5",
"version": "1.0.6",
"homepage": "https://use-http.com",
"main": "dist/index.js",
"license": "MIT",
Expand Down
50 changes: 47 additions & 3 deletions src/__tests__/useFetch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1010,11 +1010,55 @@ describe('useFetch - BROWSER - errors', (): void => {
fetch.mockResponseOnce(JSON.stringify(expectedSuccess))
})

it('should call onError when there is a network error', async (): Promise<void> => {
const onError = jest.fn()
const { waitForNextUpdate } = renderHook(
() => useFetch('https://example.com', { onError }, [])
)
await waitForNextUpdate()
expect(onError).toBeCalled()
expect(onError).toHaveBeenCalledWith({ error: expectedError })
})

it('should not call onError when aborting a request', async (): Promise<void> => {
fetch.resetMocks()
fetch.mockResponse('fail', { status: 401 })
const onError = jest.fn()
const { result, waitForNextUpdate } = renderHook(
() => useFetch('https://example.com', { onError }, [])
)
act(result.current.abort)
await waitForNextUpdate()
expect(onError).not.toBeCalled()
})

it('should only call onError on the last retry', async (): Promise<void> => {
fetch.resetMocks()
fetch.mockResponse('fail', { status: 401 })
const onError = jest.fn()
const { waitForNextUpdate } = renderHook(
() => useFetch('https://example.com/4', { onError, retries: 1 }, [])
)
await waitForNextUpdate()
expect(onError).toBeCalledTimes(1)
expect(onError).toHaveBeenCalledWith({ error: makeError(401, 'Unauthorized') })
})

it('should call onError when !response.ok', async (): Promise<void> => {
fetch.resetMocks()
fetch.mockResponse('fail', { status: 401 })
const onError = jest.fn()
const { waitForNextUpdate } = renderHook(
() => useFetch('https://example.com', { onError }, [])
)
await waitForNextUpdate()
expect(onError).toBeCalled()
expect(onError).toHaveBeenCalledWith({ error: makeError(401, 'Unauthorized') })
})

it('should set the `error` object when response.ok is false', async (): Promise<void> => {
fetch.resetMocks()
fetch.mockResponse('fail', {
status: 401
})
fetch.mockResponse('fail', { status: 401 })
const { result } = renderHook(
() => useFetch({
url: 'https://example.com',
Expand Down
1 change: 1 addition & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const useFetchArgsDefaults: UseFetchArgsReturn = {
cachePolicy: CachePolicies.CACHE_FIRST,
interceptors: {},
onAbort: () => { /* do nothing */ },
onError: () => { /* do nothing */ },
onNewData: (currData: any, newData: any) => newData,
onTimeout: () => { /* do nothing */ },
path: '',
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export interface CustomOptions {
interceptors?: Interceptors
loading?: boolean
onAbort?: () => void
onError?: OnError
onNewData?: (currData: any, newData: any) => any
onTimeout?: () => void
path?: string
Expand Down Expand Up @@ -211,12 +212,15 @@ export type RetryDelay = (<TData = any>({ attempt, error, response }: RetryOpts)
export type BodyInterfaceMethods = Exclude<FunctionKeys<Body>, 'body' | 'bodyUsed' | 'formData'>
export type ResponseType = BodyInterfaceMethods | BodyInterfaceMethods[]

export type OnError = ({ error }: { error: Error }) => void

export type UseFetchArgsReturn = {
customOptions: {
cacheLife: number
cachePolicy: CachePolicies
interceptors: Interceptors
onAbort: () => void
onError: OnError
onNewData: (currData: any, newData: any) => any
onTimeout: () => void
path: string
Expand Down
2 changes: 2 additions & 0 deletions src/useFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function useFetch<TData = any>(...args: UseFetchArgs): UseFetch<TData> {
cachePolicy, // 'cache-first' by default
interceptors,
onAbort,
onError,
onNewData,
onTimeout,
path,
Expand Down Expand Up @@ -174,6 +175,7 @@ function useFetch<TData = any>(...args: UseFetchArgs): UseFetch<TData> {
if (newRes && !newRes.ok && !error.current) error.current = makeError(newRes.status, newRes.statusText)
if (!suspense) setLoading(false)
if (attempt.current === retries) attempt.current = 0
if (error.current) onError({ error: error.current })

return data.current
} // end of doFetch()
Expand Down
26 changes: 14 additions & 12 deletions src/useFetchArgs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OptionsMaybeURL, NoUrlOptions, CachePolicies, Interceptors, OverwriteGlobalOptions, Options, RetryOn, RetryDelay, UseFetchArgsReturn, ResponseType } from './types'
import { OptionsMaybeURL, NoUrlOptions, CachePolicies, Interceptors, OverwriteGlobalOptions, Options, RetryOn, RetryDelay, UseFetchArgsReturn, ResponseType, OnError } from './types'
import { isString, isObject, invariant, pullOutRequestInit, isFunction, isPositiveNumber } from './utils'
import { useContext, useMemo } from 'react'
import FetchContext from './FetchContext'
Expand Down Expand Up @@ -62,25 +62,26 @@ export default function useFetchArgs(
}, [optionsNoURLsOrOverwriteGlobalOrDeps, deps])

const data = useField('data', urlOrOptions, optionsNoURLs)
const path = useField<string>('path', urlOrOptions, optionsNoURLs)
const timeout = useField<number>('timeout', urlOrOptions, optionsNoURLs)
const persist = useField<boolean>('persist', urlOrOptions, optionsNoURLs)
const cacheLife = useField<number>('cacheLife', urlOrOptions, optionsNoURLs)
invariant(Number.isInteger(cacheLife) && cacheLife >= 0, '`cacheLife` must be a number >= 0')
const cachePolicy = useField<CachePolicies>('cachePolicy', urlOrOptions, optionsNoURLs)
const onAbort = useField<() => void>('onAbort', urlOrOptions, optionsNoURLs)
const onTimeout = useField<() => void>('onTimeout', urlOrOptions, optionsNoURLs)
const onError = useField<OnError>('onError', urlOrOptions, optionsNoURLs)
const onNewData = useField<() => void>('onNewData', urlOrOptions, optionsNoURLs)
const onTimeout = useField<() => void>('onTimeout', urlOrOptions, optionsNoURLs)
const path = useField<string>('path', urlOrOptions, optionsNoURLs)
const perPage = useField<number>('perPage', urlOrOptions, optionsNoURLs)
const cachePolicy = useField<CachePolicies>('cachePolicy', urlOrOptions, optionsNoURLs)
const cacheLife = useField<number>('cacheLife', urlOrOptions, optionsNoURLs)
invariant(Number.isInteger(cacheLife) && cacheLife >= 0, '`cacheLife` must be a number >= 0')
const suspense = useField<boolean>('suspense', urlOrOptions, optionsNoURLs)
const persist = useField<boolean>('persist', urlOrOptions, optionsNoURLs)
const responseType = useField<ResponseType>('responseType', urlOrOptions, optionsNoURLs)
const retries = useField<number>('retries', urlOrOptions, optionsNoURLs)
invariant(Number.isInteger(retries) && retries >= 0, '`retries` must be a number >= 0')
const retryDelay = useField<RetryDelay>('retryDelay', urlOrOptions, optionsNoURLs)
invariant(isFunction(retryDelay) || Number.isInteger(retryDelay as number) && retryDelay >= 0, '`retryDelay` must be a positive number or a function returning a positive number.')
const retryOn = useField<RetryOn>('retryOn', urlOrOptions, optionsNoURLs)
const isValidRetryOn = isFunction(retryOn) || (Array.isArray(retryOn) && retryOn.every(isPositiveNumber))
invariant(isValidRetryOn, '`retryOn` must be an array of positive numbers or a function returning a boolean.')
const retryDelay = useField<RetryDelay>('retryDelay', urlOrOptions, optionsNoURLs)
invariant(isFunction(retryDelay) || Number.isInteger(retryDelay as number) && retryDelay >= 0, '`retryDelay` must be a positive number or a function returning a positive number.')
const responseType = useField<ResponseType>('responseType', urlOrOptions, optionsNoURLs)
const suspense = useField<boolean>('suspense', urlOrOptions, optionsNoURLs)
const timeout = useField<number>('timeout', urlOrOptions, optionsNoURLs)

const loading = useMemo((): boolean => {
if (isObject(urlOrOptions)) return !!urlOrOptions.loading || Array.isArray(dependencies)
Expand Down Expand Up @@ -130,6 +131,7 @@ export default function useFetchArgs(
cachePolicy,
interceptors,
onAbort,
onError,
onNewData,
onTimeout,
path,
Expand Down