Back to blog
React Custom Hooks

Creating a Custom React Hook for Fetching Data

Daniel Olowoniyi / December 25, 2024

Views: 407

In modern React development, the ability to reuse logic across components is essential for building maintainable and scalable applications. Custom hooks, a powerful feature introduced in React 16.8, allow developers to encapsulate reusable logic. This article will walk you through creating a custom useFetch hook to handle API requests with features like error handling and loading states.

Why Create a Custom Hook?

Imagine you have multiple components making API calls. Without a reusable hook, you’d likely duplicate logic for fetching data, managing loading states, and handling errors. A custom hook simplifies this process by centralizing the logic, promoting consistency, and reducing redundancy.

What We'll Build

The useFetch hook will:

Take a URL and optional configuration options as input. Return: The fetched data. Loading and error states. A refetch function for manually re-triggering the fetch.

Setting Up the Project

Start by creating a React project. You can use Vite, Create React App, or Next.js. For simplicity, let’s use Vite:

1npm create vite@latest use-fetch-hook --template react
2cd use-fetch-hook
3npm install
4npm install axios

Step 1: Creating the Custom Hook

Create a new file named useFetch.ts in your project. Here's the implementation:

useFetch Hook Implementation

1import { useState, useEffect, useCallback } from "react";
2import axios from "axios";
3
4type UseFetchResponse<T> = {
5 data: T | null;
6 isLoading: boolean;
7 error: string | null;
8 refetch: () => void;
9};
10
11function useFetch<T>(url: string, options = {}): UseFetchResponse<T> {
12 const [data, setData] = useState<T | null>(null);
13 const [isLoading, setIsLoading] = useState<boolean>(false);
14 const [error, setError] = useState<string | null>(null);
15
16 const fetchData = useCallback(async () => {
17 setIsLoading(true);
18 setError(null);
19
20 try {
21 const response = await axios.get<T>(url, options);
22 setData(response.data);
23 } catch (err: any) {
24 setError(err.message || "Something went wrong");
25 } finally {
26 setIsLoading(false);
27 }
28 }, [url, options]);
29
30 useEffect(() => {
31 fetchData();
32 }, [fetchData]);
33
34 return { data, isLoading, error, refetch: fetchData };
35}
36
37export default useFetch;

Key Features Explained

  1. State Management: Data stores the fetched data. isLoading: Indicates whether the request is in progress. error: Captures any error messages during the fetch.
  2. Fetch Functionality: The fetchData function is memoized using useCallback to prevent unnecessary re-renders.
  3. Effect Hook: Automatically fetches data when the component mounts or when the URL changes.
  4. Manual Refetching: A refetch function is provided to allow manual re-triggering of the fetch process.

Step 2: Using the Custom Hook

Here’s how you can use the useFetch hook in a component.

Example: Fetching User Data

1import React from "react";
2import useFetch from "./useFetch";
3
4const UserList = () => {
5 const { data, isLoading, error, refetch } = useFetch<{ name: string }[]>(
6 "https://jsonplaceholder.typicode.com/users"
7 );
8
9 if (isLoading) return <p>Loading...</p>;
10 if (error) return <p>Error: {error}</p>;
11
12 return (
13 <div>
14 <h1>User List</h1>
15 <ul>
16 {data?.map((user, index) => (
17 <li key={index}>{user.name}</li>
18 ))}
19 </ul>
20 <button onClick={refetch}>Refresh</button>
21 </div>
22 );
23};
24
25export default UserList;

Step 3: Customizing the Hook

The useFetch hook is versatile and can be extended with additional features like:

POST or PUT Requests: Modify the hook to support different HTTP methods.

Debouncing API Calls: Use a debounce mechanism to reduce the frequency of requests.

Caching Results: Integrate a caching strategy to reuse previously fetched data.

Here’s an example of adding support for dynamic HTTP methods:

1function useFetch<T>(
2 url: string,
3 options = {},
4 method: "GET" | "POST" | "PUT" = "GET"
5): UseFetchResponse<T> {
6 const [data, setData] = useState<T | null>(null);
7 const [isLoading, setIsLoading] = useState<boolean>(false);
8 const [error, setError] = useState<string | null>(null);
9
10 const fetchData = useCallback(async () => {
11 setIsLoading(true);
12 setError(null);
13
14 try {
15 const response = await axios({
16 url,
17 method,
18 ...options,
19 });
20 setData(response.data);
21 } catch (err: any) {
22 setError(err.message || "Something went wrong");
23 } finally {
24 setIsLoading(false);
25 }
26 }, [url, options, method]);
27
28 useEffect(() => {
29 if (method === "GET") fetchData();
30 }, [fetchData, method]);
31
32 return { data, isLoading, error, refetch: fetchData };
33}

Conclusion

Creating a custom useFetch hook encapsulates API logic into a reusable function, making your React code cleaner and more maintainable. This hook can serve as a foundation for more advanced data-fetching strategies like caching, pagination, or dynamic queries. Start implementing it in your projects to simplify API integrations and reduce boilerplate code!

Subscribe to my newsletter

Get updates on my work and projects.

We care about your data. Read our privacy policy.