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 react2cd use-fetch-hook3npm install4npm 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";34type UseFetchResponse<T> = {5 data: T | null;6 isLoading: boolean;7 error: string | null;8 refetch: () => void;9};1011function 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);1516 const fetchData = useCallback(async () => {17 setIsLoading(true);18 setError(null);1920 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]);2930 useEffect(() => {31 fetchData();32 }, [fetchData]);3334 return { data, isLoading, error, refetch: fetchData };35}3637export default useFetch;
Key Features Explained
- State Management: Data stores the fetched data. isLoading: Indicates whether the request is in progress. error: Captures any error messages during the fetch.
- Fetch Functionality: The fetchData function is memoized using useCallback to prevent unnecessary re-renders.
- Effect Hook: Automatically fetches data when the component mounts or when the URL changes.
- 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";34const UserList = () => {5 const { data, isLoading, error, refetch } = useFetch<{ name: string }[]>(6 "https://jsonplaceholder.typicode.com/users"7 );89 if (isLoading) return <p>Loading...</p>;10 if (error) return <p>Error: {error}</p>;1112 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};2425export 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);910 const fetchData = useCallback(async () => {11 setIsLoading(true);12 setError(null);1314 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]);2728 useEffect(() => {29 if (method === "GET") fetchData();30 }, [fetchData, method]);3132 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!
