custom hooks

useFetch( )

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Explanation

The custom hook useFetch provides a seamless solution for fetching data in React components, with a notable feature being caching. By encapsulating common asynchronous logic, this hook efficiently manages the state of fetch operations, including status updates, error handling, and data retrieval.

The caching mechanism enhances performance by storing previously fetched data, optimizing the application by minimizing redundant network requests, thereby improving overall responsiveness and reducing server load. Notably, the cache is stored in useRef throughout the component's lifecycle, remaining accessible and preserved as long as the component is active and not unmounted. By efficiently managing the cache in this manner, useFetch ensures fast and efficient data retrieval, contributing to a smoother user experience.

Incorporating useReducer and useEffect hooks internally, useFetch elegantly handles various aspects of data fetching. It seamlessly transitions between different states — "idle", "fetching", "fetched", or "error" — based on the outcome of the fetch operation. Additionally, it gracefully manages component unmounts, ensuring clean-up and preventing potential memory leaks by canceling ongoing requests.

By abstracting away the complexities of data fetching and state management, useFetch empowers developers to integrate asynchronous data fetching into their React applications with ease, significantly reducing boilerplate code and enhancing development efficiency.

Usage Cases

  • Fetching User Data: Retrieve user information from an API endpoint based on selected criteria, such as user ID or username.
  • Displaying Loading Indicators: Implement loading indicators to provide visual feedback to users while fetching data from the server.
  • Handling Errors: Manage error states gracefully by displaying error messages when data fetching encounters issues, ensuring a smooth user experience.
  • Dynamic Content Rendering: Dynamically render content based on the fetched data, enabling personalized and responsive user interfaces.
  • Optimizing Performance with Caching: Utilize caching functionality to store previously fetched data, reducing redundant network requests and improving application responsiveness.
  • Efficient Data Management: Streamline data fetching and state management logic in React components, minimizing boilerplate code and enhancing development efficiency.

Creation

Code implementation
1enum Status {
2 IDLE = 'idle',
3 FETCHING = 'fetching',
4 FETCHED = 'fetched',
5 ERROR = 'error',
6}
7
8// Define action types excluding IDLE status
9type ActionTypes = Exclude<Status, Status.IDLE>;
10
11export interface State {
12 status: Status;
13 errorMessage: string;
14 data: unknown;
15}
16
17interface Action {
18 type: ActionTypes;
19 payload?: unknown;
20}
21
22const initialState: State = {
23 status: Status.IDLE,
24 errorMessage: '',
25 data: [],
26};
27
28const reducer = (state: State, action: Action): State => {
29 switch (action.type) {
30 case Status.FETCHING:
31 // Set status to fetching
32 return { ...state, status: Status.FETCHING };
33 case Status.FETCHED:
34 // Set status to fetched and update data
35 return { ...state, status: Status.FETCHED, data: action.payload };
36 case Status.ERROR:
37 // Set status to error and update error message
38 return {
39 ...state,
40 status: Status.ERROR,
41 errorMessage: action.payload as string,
42 };
43 default:
44 // Return current state for unknown actions
45 return state;
46 }
47};
48
49const useFetch = (url: string): State => {
50 // Cache for storing fetched data
51 const cacheRef = useRef<{ [key: string]: unknown }>({});
52
53 // Reference to abort controller for cancelling requests
54 const abortControllerRef = useRef<AbortController | null>(null);
55
56 // Use `useReducer` hook for managing state
57 const [state, dispatch] = useReducer(reducer, initialState);
58
59 useEffect(() => {
60 // Create flag to cancel request on component unmount
61 let cancelRequest = false;
62
63 // Ignore empty or whitespace-only URLs
64 if (!url || !url.trim()) return;
65
66 const fetchData = async (): Promise<void> => {
67 // Dispatch fetching action
68 dispatch({ type: Status.FETCHING });
69
70 // Check if the data for this URL is already cached
71 if (cacheRef.current[url]) {
72 // Retrieve data from cache if available
73 const data = cacheRef.current[url];
74
75 // Dispatch fetched action with cached data
76 dispatch({ type: Status.FETCHED, payload: data });
77 } else {
78 try {
79 // Create a new `AbortController` and save it in `useRef`
80 const controller = new AbortController();
81 abortControllerRef.current = controller;
82
83 // Make the network request and pass the abort signal
84 const response = await fetch(url, {
85 signal: controller.signal,
86 });
87
88 // Throw an error if the response is not successful
89 if (!response.ok) {
90 throw new Error('Failed to fetch data');
91 }
92
93 // Parse response data
94 const data = await response.json();
95
96 // Cache fetched data
97 cacheRef.current[url] = data;
98
99 // If request was cancelled, do nothing
100 if (cancelRequest) return;
101
102 // Dispatch fetched action with retrieved data
103 dispatch({ type: Status.FETCHED, payload: data });
104 } catch (error) {
105 // If request was cancelled, do nothing
106 if (cancelRequest) return;
107
108 // If error is an instance of `Error`, get error message. Otherwise, convert to string
109 const message =
110 error instanceof Error ? error.message : String(error);
111
112 // Dispatch error action and pass error message as payload
113 dispatch({
114 type: Status.ERROR,
115 payload: message,
116 });
117 }
118 }
119 };
120
121 // Call `fetchData` function on component mount
122 fetchData();
123
124 return (): void => {
125 // Set `cancelRequest` flag to `true` on component unmount
126 cancelRequest = true;
127
128 if (abortControllerRef.current) {
129 // Cancel previous request on component unmount
130 abortControllerRef.current.abort();
131 }
132 };
133 }, [url]); // Re-run `useEffect` only when `url` changes
134
135 return state;
136};
137
138export default useFetch;
139

Reference

Code implementation
1const { status, data, errorMessage } = useFetch(url);
2

Parameters

NameTypeDescription
urlstringThe URL of the API endpoint to fetch data from.

Return values

NameTypeDescription
statusStatusThe current status of the fetch operation ("idle", "fetching", "fetched", or "error")
errorMessagestringThe error message if an error occurred during the request.
dataunknownThe fetched data from the API endpoint.

Example Usages

Fetching User Data

The Container component utilizes the useFetch hook to retrieve user data using the jsonplaceholder API. Users can select a person's identifier from a list, and the corresponding biographical information is fetched and displayed. The component also renders a loading indicator while fetching data and an error message in case of failure. If a user selects an identifier that has been requested previously, the data is retrieved from the cache, resulting in faster information display than sending a request to the server.

Code implementation
1// Options for person selection
2const idsList = Array.from({ length: 4 }, (_, index) => ({
3 id: index + 1,
4 name: `User Id: ${index + 1}`,
5}));
6
7// Adding user with non-existent ID to show error message
8idsList.push({
9 id: 25,
10 name: `Non-existent user`,
11});
12
13const Container: FC = () => {
14 // State to manage selected person ID
15 const [personId, setPersonId] = useState(1);
16
17 // Fetch user data based on selected person ID using custom hook
18 const { status, data, errorMessage }: State = useFetch(
19 `https://jsonplaceholder.typicode.com/users/${personId}`
20 );
21
22 // Handler to update the selected person ID
23 const handleChange = (item: (typeof idsList)[0]) => {
24 setPersonId(item.id);
25 };
26
27 return (
28 <Layer title="Container">
29 <List
30 items={idsList}
31 selected={personId}
32 onClick={handleChange}
33 />
34 <div className="flex w-full justify-center">
35 {/* Display a loader while fetching data */}
36 {status === 'fetching' && <Loader />}
37
38 {/* Display an error message in case of error */}
39 {status === 'error' && (
40 <Message type={MessageType.Error}>{errorMessage}</Message>
41 )}
42 {/* Display bio information once fetched */}
43 {status === 'fetched' && (
44 <PersonBio {...(data as FullUserData)} />
45 )}
46 </div>
47 </Layer>
48 );
49};
50
An interactive output of the code above
Last updated on by @skrykateSuggest an improvement on Github repository.