useFetch()
Here is a React Hook which aims to retrieve data on an API using the native Fetch API.
I used a reducer to separate state logic and simplify testing via functional style.
The received data is saved (cached) in the application via useRef, but you can use LocalStorage (see useLocalStorage()
) or a caching solution to persist the data.
The fetch is executed when the component is mounted and if the url changes. If ever the url is undefined, or if the component is unmounted before the data is recovered, the fetch will not be called.
This hook also takes the request config as a second parameter in order to be able to pass the authorization token in the header of the request, for example. Be careful though, the latter does not trigger a re-rendering in case of modification, go through the url params to dynamically change the request.
Side notes:
- To understand how is working this hook, you can read this article from "Smashing Magazine" which explains how to build a custom react hook to fetch and cache data
- For usage in SSR, consider using window.fetch.polyfill
- It's a very simple fetch hook for basic use cases and learning purposes. For advanced usages and optimisations, see these other hooks more powerfull like useSWR, useQuery or if you're using Redux Toolkit, consider RTK Query.
The Hook
1import { useEffect, useReducer, useRef } from 'react'23interface State<T> {4 data?: T5 error?: Error6}78type Cache<T> = { [url: string]: T }910// discriminated union type11type Action<T> =12 | { type: 'loading' }13 | { type: 'fetched'; payload: T }14 | { type: 'error'; payload: Error }1516function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {17 const cache = useRef<Cache<T>>({})1819 // Used to prevent state update if the component is unmounted20 const cancelRequest = useRef<boolean>(false)2122 const initialState: State<T> = {23 error: undefined,24 data: undefined,25 }2627 // Keep state logic separated28 const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {29 switch (action.type) {30 case 'loading':31 return { ...initialState }32 case 'fetched':33 return { ...initialState, data: action.payload }34 case 'error':35 return { ...initialState, error: action.payload }36 default:37 return state38 }39 }4041 const [state, dispatch] = useReducer(fetchReducer, initialState)4243 useEffect(() => {44 // Do nothing if the url is not given45 if (!url) return4647 cancelRequest.current = false4849 const fetchData = async () => {50 dispatch({ type: 'loading' })5152 // If a cache exists for this url, return it53 if (cache.current[url]) {54 dispatch({ type: 'fetched', payload: cache.current[url] })55 return56 }5758 try {59 const response = await fetch(url, options)60 if (!response.ok) {61 throw new Error(response.statusText)62 }6364 const data = (await response.json()) as T65 cache.current[url] = data66 if (cancelRequest.current) return6768 dispatch({ type: 'fetched', payload: data })69 } catch (error) {70 if (cancelRequest.current) return7172 dispatch({ type: 'error', payload: error as Error })73 }74 }7576 void fetchData()7778 // Use the cleanup function for avoiding a possibly...79 // ...state update after the component was unmounted80 return () => {81 cancelRequest.current = true82 }83 // eslint-disable-next-line react-hooks/exhaustive-deps84 }, [url])8586 return state87}8889export default useFetch
Usage
1import { useFetch } from 'usehooks-ts'23const url = `http://jsonplaceholder.typicode.com/posts`45interface Post {6 userId: number7 id: number8 title: string9 body: string10}1112export default function Component() {13 const { data, error } = useFetch<Post[]>(url)1415 if (error) return <p>There is an error.</p>16 if (!data) return <p>Loading...</p>17 return <p>{data[0].title}</p>18}
See a way to make this page better?
Edit there »