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:
npm create vite@latest use-fetch-hook --template react
cd use-fetch-hook
npm install
npm 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
import { useState, useEffect, useCallback } from "react";
import axios from "axios";
type UseFetchResponse<T> = {
data: T | null;
isLoading: boolean;
error: string | null;
refetch: () => void;
};
function useFetch<T>(url: string, options = {}): UseFetchResponse<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await axios.get<T>(url, options);
setData(response.data);
} catch (err: any) {
setError(err.message || "Something went wrong");
} finally {
setIsLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, isLoading, error, refetch: fetchData };
}
export 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
import React from "react";
import useFetch from "./useFetch";
const UserList = () => {
const { data, isLoading, error, refetch } = useFetch<{ name: string }[]>(
"https://jsonplaceholder.typicode.com/users"
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h1>User List</h1>
<ul>
{data?.map((user, index) => (
<li key={index}>{user.name}</li>
))}
</ul>
<button onClick={refetch}>Refresh</button>
</div>
);
};
export 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:
function useFetch<T>(
url: string,
options = {},
method: "GET" | "POST" | "PUT" = "GET"
): UseFetchResponse<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await axios({
url,
method,
...options,
});
setData(response.data);
} catch (err: any) {
setError(err.message || "Something went wrong");
} finally {
setIsLoading(false);
}
}, [url, options, method]);
useEffect(() => {
if (method === "GET") fetchData();
}, [fetchData, method]);
return { data, isLoading, error, refetch: fetchData };
}
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!