Back to blog
Creating a Custom React Hook for Fetching Data

Creating a Custom React Hook for Fetching Data

Daniel Olowoniyi / December 25, 2024

Views: 32

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!

Subscribe to my newsletter

Get updates on my work and projects.

We care about your data. Read our privacy policy.