useFetch()

Here is a React Hook which aims to retrieve data on a Rest API using the Axios library.

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 Axios option object 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.

Sources:

You can read this article from "Smashing Magazine" which explains how to build a custom react hook to fetch and cache data

The Hook

1import { useEffect, useReducer, useRef } from 'react'
2
3import axios, { AxiosRequestConfig } from 'axios'
4
5// State & hook output
6interface State<T> {
7 status: 'init' | 'fetching' | 'error' | 'fetched'
8 data?: T
9 error?: string
10}
11
12interface Cache<T> {
13 [url: string]: T
14}
15
16// discriminated union type
17type Action<T> =
18 | { type: 'request' }
19 | { type: 'success'; payload: T }
20 | { type: 'failure'; payload: string }
21
22function useFetch<T = unknown>(
23 url?: string,
24 options?: AxiosRequestConfig,
25): State<T> {
26 const cache = useRef<Cache<T>>({})
27 const cancelRequest = useRef<boolean>(false)
28
29 const initialState: State<T> = {
30 status: 'init',
31 error: undefined,
32 data: undefined,
33 }
34
35 // Keep state logic separated
36 const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
37 switch (action.type) {
38 case 'request':
39 return { ...initialState, status: 'fetching' }
40 case 'success':
41 return { ...initialState, status: 'fetched', data: action.payload }
42 case 'failure':
43 return { ...initialState, status: 'error', error: action.payload }
44 default:
45 return state
46 }
47 }
48
49 const [state, dispatch] = useReducer(fetchReducer, initialState)
50
51 useEffect(() => {
52 if (!url) {
53 return
54 }
55
56 const fetchData = async () => {
57 dispatch({ type: 'request' })
58
59 if (cache.current[url]) {
60 dispatch({ type: 'success', payload: cache.current[url] })
61 } else {
62 try {
63 const response = await axios(url, options)
64 cache.current[url] = response.data
65
66 if (cancelRequest.current) return
67
68 dispatch({ type: 'success', payload: response.data })
69 } catch (error) {
70 if (cancelRequest.current) return
71
72 dispatch({ type: 'failure', payload: error.message })
73 }
74 }
75 }
76
77 fetchData()
78
79 return () => {
80 cancelRequest.current = true
81 }
82 // eslint-disable-next-line react-hooks/exhaustive-deps
83 }, [url])
84
85 return state
86}
87
88export default useFetch

Usage

1import React from 'react'
2
3import useFetch from './useFetch'
4
5interface Post {
6 userId: number
7 id: number
8 title: string
9 body: string
10}
11
12export default function Component() {
13 const url = `http://jsonplaceholder.typicode.com/posts`
14 const { status, data, error } = useFetch<Post[]>(url)
15 console.log({ status, data, error })
16
17 // your component JSX
18 return <div>{status}</div>
19}

See a way to make this page better?
Edit there »