DEV Community

Cover image for How to Create a Custom Hook for Seamless Data Retrieval with React.
Makanju Oluwafemi
Makanju Oluwafemi

Posted on

How to Create a Custom Hook for Seamless Data Retrieval with React.

introduction

Custom hooks in react allow developers to extract logic that is needed in multiple components into a standalone function. This can apply to making API requests, state management, or even side effects.

Side effects in React refer to any operation that occurs outside the scope of the component's render function, such as data fetching, subscriptions, or DOM manipulation.

There are a few rules that govern hooks creation in React; you can check them [here]: https://legacy.reactjs.org/docs/hooks-rules. However, make sure your hook name starts with use.

Create a Service That Handles API Requests

When working on features that require logic duplication, for example, you need to send a GET request to /api/allproduct, /api/singleproduct/:id and so on. You can decide to create a service that handles all these requests in a single file and export them to use in another file.

To understand what I'm saying in essence, walk with me!
Do ensure that you have a React project set up already, or copy this code to do that.

npx create-react-app hooks-example
cd hooks-example
Enter fullscreen mode Exit fullscreen mode

Also, copy this code to add axios

npm i axios
Enter fullscreen mode Exit fullscreen mode

Now that you are all set, create a folder and name it service in your src directory. Inside this folder, create an
evenService.js file.

eventService.js

import axios from "axios";

const baseURL = 'api-url/';

const apiService = axios.create({
    baseURL,
    headers: {
        'Content-Type': 'application/json',
    }
});

export const fetchAllProduct = async () => {
    try {
        const res = await apiService.get('api/all-products');
        if (res.data.status === 200) {
            return res.data.products; // Return only the products data
        } else {
            throw new Error(`Failed to fetch all products: ${res.data.message}`); // Include more descriptive error message
        }
    } catch (error) {
        throw new Error('Failed to fetch all products. Please try again.'); 
    }
};
Enter fullscreen mode Exit fullscreen mode

In this example code, I have defined an Axios instance apiService with a baseURL and default headers for making HTTP requests to an API. It also defines a function fetchAllProduct for fetching all products from the API using a GET request.

Is this approach better?

I won't say it's not better, but then things can be implemented better by putting this logic into a custom hook called useApi

Here is my take
If I continue like this, the eventService.js file will soon become so long and unorganized. I will have to add a new export function for each API request. Rewriting the async awaits for everyone of the function. 
This does not seem to be perfect; remember the DRY (Do Not Repeat Yourself) rule.

Create a Custom Hook for API requests

You can edit the eventService.js file by removing the fetchAllProduct function.

Then add export to apiService like this:

import axios from "axios";

const baseURL = 'api-url/';

export const apiService = axios.create({
    baseURL,
    headers: {
        'Content-Type': 'application/json',
    }
});
Enter fullscreen mode Exit fullscreen mode

Create a folder and name it hooks in your src directory. Then, create a useApi.js file. Import your apiService and use it here.

Copy and paste this code.

import { useState, useEffect } from "react";
import  { apiService } from '../service'

const useApi = (endpoint, method = 'GET', req = null) => {
   const [loading, setLoading] = useState(true);
   const [data, setData] = useState(null);
   const [error, setError] = useState(null);

   useEffect(() => {
     const handleEndpoint = async () => {
       setLoading(true);
       try {
         let response;
         switch(method.toUpperCase()) {
           case 'GET':
             response = await apiService.get(endpoint);
             break;
           case 'POST':
             response = await apiService.post(endpoint, req);
             break;
           case 'PUT':
             response = await apiService.put(endpoint, req);
             break;
           case 'DELETE':
             response = await apiService.delete(endpoint);
             break;
           default:
             throw new Error(`Unsupported HTTP method: ${method}`);
         }
         setData(response.data);
       } catch (error) {
         setError(error);
       } finally {
        setLoading(false);
       }
     };

     handleEndpoint();
   }, [endpoint, method, req]);

   return { loading, data, error };
};

export default useApi;
Enter fullscreen mode Exit fullscreen mode

In this example, we created a custom hook called useApi. It encapsulates API fetching logic in a reusable manner. useState and useEffect hooks are used to manage loading state, fetched data, and potential errors. When invoked with a specified API endpoint, HTTP method, and optional request body (req), it triggers an asynchronous operation to fetch data from the API using the provided method (GET, POST, PUT, DELETE). Upon completion, it updates the component's state accordingly, setting loading to false, storing the retrieved data in data, and capturing any errors in error.

This hook allows reusability, reduces the complexities of API interaction, and promotes cleaner code.

With these few points of mine, I hope I have been able to convince and not confuse you that creating a custom hook for your API request will significantly advance modularity and reusability. Happy Coding!

Top comments (1)

Collapse
 
devyoma profile image
Emore Ogheneyoma Lawrence

Nice article 🚀. Interesting read 💯