Appearance
useFetch
Reactive Fetch API provides the ability to abort requests, intercept requests before they are fired, automatically refetch requests when the url changes, and create your own useFetch with predefined options.
Demo
Usage
Basic Usage
The useFetch function can be used by simply providing a url. The url can be either a string or a controllable state. The data value will contain the result of the request, the error value will contain any errors, and the isFetching value will indicate if the request is loading.
ts
import { useFetch } from '@reause/core'
const { isFetching, error, data } = useFetch(url)Asynchronous Usage
useFetch can also be awaited just like a normal fetch:
ts
const { isFetching, error, data } = await useFetch(url)Refetching on URL change
Using a plain value for the url parameter (e.g. driven by useState) will allow the useFetch function to automatically trigger another request when the url changes.
tsx
import { useFetch } from '@reause/core'
import { useState } from 'react'
const [url, setUrl] = useState('https://my-api.com/user/1')
const { data } = useFetch(url, { refetch: true })
setUrl('https://my-api.com/user/2') // Will trigger another requestPrevent request from firing immediately
Setting the immediate option to false will prevent the request from firing until the execute function is called.
ts
const { execute } = useFetch(url, { immediate: false })
execute()Aborting a request
A request can be aborted by using the abort function from the useFetch function. The canAbort property indicates if the request can be aborted.
ts
const { abort, canAbort } = useFetch(url)
setTimeout(() => {
if (canAbort)
abort()
}, 100)A request can also be aborted automatically by using timeout property. It will call abort function when the given timeout is reached.
ts
const { data } = useFetch(url, { timeout: 100 })Intercepting a request
The beforeFetch option can intercept a request before it is sent and modify the request options and url.
ts
const { data } = useFetch(url, {
async beforeFetch({ url, options, cancel }) {
const myToken = await getMyToken()
if (!myToken)
cancel()
options.headers = {
...options.headers,
Authorization: `Bearer ${myToken}`,
}
return {
options,
}
},
})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 response data
return ctx
},
})The onFetchError option can intercept the response data and error before it is updated when updateDataOnError is set to true.
ts
const { data } = useFetch(url, {
updateDataOnError: true,
onFetchError(ctx) {
// ctx.data can be null when 5xx response
if (ctx.data === null)
ctx.data = { title: 'Hunter x Hunter' } // Modifies the response data
ctx.error = new Error('Custom Error') // Modifies the error
return ctx
},
})
console.log(data) // { title: 'Hunter x Hunter' }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
ts
// Request will be sent with GET method and data will be parsed as JSON
const { data } = useFetch(url).get().json()
// Request will be sent with POST method and data will be parsed as text
const { data } = useFetch(url).post().text()
// Or set the method using the options
// Request will be sent with GET method and data will be parsed as blob
const { data } = useFetch(url, { method: 'GET' }, { refetch: true }).blob()Creating a Custom Instance
The createFetch function will return a useFetch function with whatever pre-configured options that are provided to it. This is useful for interacting with API's throughout an application that uses the same base URL or needs Authorization headers.
ts
const useMyFetch = createFetch({
baseUrl: 'https://my-api.com',
options: {
async beforeFetch({ options }) {
const myToken = await getMyToken()
options.headers.Authorization = `Bearer ${myToken}`
return { options }
},
},
fetchOptions: {
mode: 'cors',
},
})
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,
}
},
})You can re-execute the request by calling the execute method in afterFetch or onFetchError. Here is a simple example of refreshing a token:
ts
let isRefreshing = false
const refreshSubscribers: Array<() => void> = []
const useMyFetch = createFetch({
baseUrl: 'https://my-api.com',
options: {
async beforeFetch({ options }) {
const myToken = await getMyToken()
options.headers.Authorization = `Bearer ${myToken}`
return { options }
},
afterFetch({ data, response, context, execute }) {
if (needRefreshToken) {
if (!isRefreshing) {
isRefreshing = true
refreshToken().then((newToken) => {
if (newToken) {
isRefreshing = false
setMyToken(newToken)
onRefreshed()
}
else {
refreshSubscribers.length = 0
// handle refresh token error
}
})
}
return new Promise((resolve) => {
addRefreshSubscriber(() => {
execute().then((response) => {
resolve({ data, response })
})
})
})
}
return { data, response }
},
// or use onFetchError with updateDataOnError
updateDataOnError: true,
onFetchError({ error, data, response, context, execute }) {
// same as afterFetch
return { error, data }
},
},
fetchOptions: {
mode: 'cors',
},
})
async function refreshToken() {
const { data, execute } = useFetch<string>('refresh-token', {
immediate: false,
})
await execute()
return data
}
function onRefreshed() {
refreshSubscribers.forEach(callback => callback())
refreshSubscribers.length = 0
}
function addRefreshSubscriber(callback: () => void) {
refreshSubscribers.push(callback)
}
const { isFetching, error, data } = useMyFetch('users')Events
The onFetchResponse 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)
})Type Declarations
Toggle
ts
export interface UseFetchReturn<T> {
isFinished: boolean;
statusCode: number | null;
setStatusCode: Dispatch<SetStateAction<number | null>>;
response: Response | null;
setResponse: Dispatch<SetStateAction<Response | null>>;
error: any;
setError: Dispatch<SetStateAction<any>>;
data: T | null;
setData: Dispatch<SetStateAction<T | null>>;
isFetching: boolean;
canAbort: boolean;
aborted: boolean;
setAborted: Dispatch<SetStateAction<boolean>>;
abort: (reason?: any) => void;
execute: (throwOnFailed?: boolean) => Promise<any>;
onFetchResponse: EventHookOn<Response>;
onFetchError: EventHookOn;
onFetchFinally: EventHookOn;
get: () => UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
post: (payload?: unknown, type?: string) => UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
put: (payload?: unknown, type?: string) => UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
delete: (payload?: unknown, type?: string) => UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
patch: (payload?: unknown, type?: string) => UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
head: (payload?: unknown, type?: string) => UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
options: (payload?: unknown, type?: string) => UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
json: <JSON = any>() => UseFetchReturn<JSON> & PromiseLike<UseFetchReturn<JSON>>;
text: () => UseFetchReturn<string> & PromiseLike<UseFetchReturn<string>>;
blob: () => UseFetchReturn<Blob> & PromiseLike<UseFetchReturn<Blob>>;
arrayBuffer: () => UseFetchReturn<ArrayBuffer> & PromiseLike<UseFetchReturn<ArrayBuffer>>;
formData: () => UseFetchReturn<FormData> & PromiseLike<UseFetchReturn<FormData>>;
}
export interface BeforeFetchContext {
url: string;
options: RequestInit;
cancel: () => void;
}
export interface AfterFetchContext<T = any> {
response: Response;
data: T | null;
context: BeforeFetchContext;
execute: (throwOnFailed?: boolean) => Promise<any>;
}
export interface OnFetchErrorContext<T = any, E = any> {
error: E;
data: T | null;
response: Response | null;
context: BeforeFetchContext;
execute: (throwOnFailed?: boolean) => Promise<any>;
}
export interface UseFetchOptions {
fetch?: typeof window.fetch;
immediate?: boolean;
refetch?: boolean;
initialData?: any;
timeout?: number;
updateDataOnError?: boolean;
beforeFetch?: (ctx: BeforeFetchContext) => Promise<Partial<BeforeFetchContext> | void> | Partial<BeforeFetchContext> | void;
afterFetch?: (ctx: AfterFetchContext) => Promise<Partial<AfterFetchContext>> | Partial<AfterFetchContext>;
onFetchError?: (ctx: OnFetchErrorContext) => Promise<Partial<OnFetchErrorContext>> | Partial<OnFetchErrorContext>;
}
export interface CreateFetchOptions {
baseUrl?: string;
combination?: Combination;
options?: UseFetchOptions;
fetchOptions?: RequestInit;
}
type EventHookOn<T = any> = (fn: (param: T) => void) => () => void
type Combination = 'overwrite' | 'chain'
export function createFetch(config?: CreateFetchOptions)
export function useFetch<T>(url: string): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>
export function useFetch<T>(url: string, useFetchOptions: UseFetchOptions): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>
export function useFetch<T>(url: string, options: RequestInit, useFetchOptions?: UseFetchOptions): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>