useFetch
A custom hook for fetching data from an API.
Add hook
Create a file use-fetch.ts
and copy & paste the code from useFetch.
Hook
// Currently,this hook does not deal with caching, de-bouncing or de-duping
import { useCallback, useEffect, useRef, useState } from 'react'
type UseFetchResponse<T> = {
data?: T
loading?: boolean
error?: Error
refetch: (options?: RequestInit) => Promise<{ data?: T; error?: Error; aborted?: boolean }>
}
type UseFetch = {
<T>(url: string): UseFetchResponse<T>
<T>(url: string, trigger: boolean): UseFetchResponse<T>
<T>(url: string, requestOptions: RequestInit): UseFetchResponse<T>
<T>(url: string, trigger: boolean, requestOptions: RequestInit): UseFetchResponse<T>
}
const deepEqual = (a: unknown, b: unknown): boolean => {
if (a === b) {
return true
}
if (!(a instanceof Object) || !(b instanceof Object)) {
return false
}
if (a.constructor !== b.constructor) {
return false
}
const aKeys = Object.keys(a)
const bKeys = Object.keys(b)
if (aKeys.length !== bKeys.length || aKeys.some(key => !bKeys.includes(key))) {
return false
}
return aKeys.every(key => deepEqual(a[key as keyof typeof a], b[key as keyof typeof b]))
}
export const useFetch: UseFetch = <T = unknown>(
url: string,
trigger?: boolean | RequestInit,
options?: RequestInit,
): UseFetchResponse<T> => {
const requestOptions = typeof trigger === 'boolean' ? options : trigger || {}
const hasTrigger = typeof trigger === 'boolean' ? trigger : false
const abortControllerRef = useRef<AbortController>()
const requestOptionsRef = useRef<unknown>(requestOptions)
const requestOptionsChanged = useRef(Symbol())
if (!deepEqual(requestOptions, requestOptionsRef.current)) {
requestOptionsRef.current = requestOptions
requestOptionsChanged.current = Symbol()
}
const [error, setError] = useState<Error>()
const [data, setData] = useState<T>()
const [loading, setLoading] = useState(false)
const fetchData = useCallback(
async (options: RequestInit = {}) => {
setLoading(true)
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const abortController = new AbortController()
abortControllerRef.current = abortController
try {
const response = await fetch(url, { signal: abortController.signal, ...requestOptions, ...options })
if (abortControllerRef.current !== abortController) {
// this should never happen because the call should have been aborted and fallen into the error handler
return { aborted: true }
}
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
const result: T = await response.json()
setData(result)
return { data: result }
} catch (error) {
if (abortControllerRef.current !== abortController) {
return { aborted: true }
}
setData(undefined)
let normalisedError = new Error('Unknown Error')
if (error instanceof Error) {
normalisedError = error
}
if (typeof error === 'string') {
normalisedError = new Error(error)
}
setError(normalisedError)
return { error: normalisedError }
} finally {
if (abortControllerRef.current === abortController) {
setLoading(false)
abortControllerRef.current = undefined
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[url, requestOptionsChanged.current],
)
useEffect(() => {
if (!hasTrigger) {
fetchData()
}
}, [hasTrigger, fetchData])
return { data, loading, error, refetch: fetchData }
}
Usage
import React, { useEffect, useState } from 'react';
import { useFetch } from './hooks/use-fetch';
type Product = {
id: string
title: string
}
function App() {
const { data, isLoading, error, refetch } = useFetch<Product[]>('https://fakestoreapi.com/products');
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>useFetch Example</h1>
<p>Data: {JSON.stringify(data)}</p>
<button onClick={refetch}>Refetch Data</button>
</div>
);
}
export default App;
API
Parameters
Name | Type | Description |
---|---|---|
url | string | The URL from which to fetch the data. |
trigger | boolean (optional) | If true , the fetch operation will be triggered immediately. Default is true . |
requestOptions | RequestInit (optional) | Options to pass to the fetch function. |
Returns
Name | Type | Description |
---|---|---|
data | DataType | undefined | The fetched data, if available. |
isLoading | boolean | Indicates whether the fetch operation is in progress. |
error | Error | undefined | An error object, if an error occurred during the fetch operation. |
refetch | () => Promise<void> | A function to manually trigger the fetch operation. |
trigger | () => void | A function to manually trigger the fetch operation. |