Transform Your React App’s Backend Communication with useHttpClient
Tags: react, tutorial, frontend, fetch
Published: 2024-03-25 18:33:22

In React application development, a common practice is to communicate with a back-end to perform CRUD operations. However, this process often involves writing and rewriting similar fetch logic across different components or services, leading to **repetitive**, **hard-to-maintain code** that's prone to errors. But what if there was a more elegant and efficient way to handle this communication?

###Table of Contents
1 [The Challenge](#the-challenge)
2 [Understanding useHttpClient](#understanding-usehttpclient)
3 [Why Is It a Better Solution?](#why-is-it-a-better-solution)
4 [Implementation in Action](#implementation-in-action)
5 [Conclusion](#conclusion)

---

###The Challenge:
Imagine having to implement multiple fetch calls in your app, each slightly different from the next. Over time, this approach can lead to a cluttered codebase, where managing changes becomes a nightmare, and the likelihood of introducing errors increases with each new request.

**The Solution: Utilize Custom Hooks**

This is where our approach changes the game. I propose the introduction of a custom hook in React: **useHttpClient**. This hook is designed to abstract the HTTP request logic, centralizing back-end communication in a **reusable**, **easy-to-maintain** place.
![useHttpClient-applied](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jjavoqlj0ywxlfhog3z6.png)

###Understanding useHttpClient:
**useHttpClient** encapsulates the complexity of fetch requests, allowing developers to focus on business logic rather than on the details of server communication. By defining specific methods for each request type (GET, POST, PUT, DELETE), this hook offers a clear and concise interface for interacting with the back-end.
```react
interface useHttpClientProps {
    endpoint: string;
}

export interface CustomRequestProps {
    body?: object; 
    endpoint?: string; 
    params?: Record<string, string>;
}

export const useHttpClient = (props: useHttpClientProps)  => {

    const baseUrl = process.env.REACT_APP_BACKEND_URL as string ?? "http://localhost:8080/api";

    const prepareRequest = async (httpMethod: string, bodyObject?: object): Promise<RequestInit> => {
        const requestOptions: RequestInit = {
            method: httpMethod,
            headers: {
                'Content-type': 'application/json',
            },
            body: bodyObject ? JSON.stringify(bodyObject) : null,
        };

        return requestOptions;
    }

    const executeRequest = async <T>(request: RequestInit, endpoint?: string, params?: Record<string, string>): Promise<T> => {
        const queryParams = new URLSearchParams(params).toString();
        const requestUrl: string = baseUrl + props.endpoint + (endpoint ?? '') + (params ? `?${queryParams}` : '');        
        try {
            const response = await fetch(requestUrl, request);
            if (!response.ok) {
                const errorMsg = await response.text();
                throw new Error(errorMsg);
            }

            const contentType = response.headers.get('Content-Type');
            if (contentType && contentType.includes('application/json')) {
                return await response.json() as T;
            } else {
                return {} as T;
            }
            
        } catch (error) {
            console.error(error);
            throw error;
        }
    }

    const get = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("GET", body);
        return executeRequest(request, endpoint, params);
    }

    const post = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("POST", body);
        return executeRequest(request, endpoint, params);
    }

    const put = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("PUT", body);
        return executeRequest(request, endpoint, params);
    }

    const del = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("DELETE", body);
        return executeRequest(request, endpoint, params);
    }

    return { get, post, put, del };
}
```

Now that we see the bigger picture, let's break this down:
At the heart of our custom hook, **useHttpClient**, lie two critical functions: **prepareRequest** and **executeRequest**. These functions streamline the process of making HTTP requests, handling the intricacies of request preparation and execution.

####prepareRequest
```react
const prepareRequest = async (httpMethod: string, bodyObject?: object): Promise<RequestInit> => {
        const requestOptions: RequestInit = {
            method: httpMethod,
            headers: {
                'Content-type': 'application/json',
            },
            body: bodyObject ? JSON.stringify(bodyObject) : null,
        };

        return requestOptions;
    }
```
The **prepareRequest** function constructs the options for a fetch request. It takes the HTTP method and an optional body object as arguments. The method parameter dictates the type of request (e.g., GET, POST), while the body object, if present, is stringified and included in the request options. This setup ensures that all requests are sent with the appropriate headers and payload format, laying the groundwork for effective communication with the backend.

####executeRequest
```react
const executeRequest = async <T>(request: RequestInit, endpoint?: string, params?: Record<string, string>): Promise<T> => {
        const queryParams = new URLSearchParams(params).toString();
        const requestUrl: string = baseUrl + props.endpoint + (endpoint ?? '') + (params ? `?${queryParams}` : '');        
        try {
            const response = await fetch(requestUrl, request);
            if (!response.ok) {
                const errorMsg = await response.text();
                throw new Error(errorMsg);
            }

            const contentType = response.headers.get('Content-Type');
            if (contentType && contentType.includes('application/json')) {
                return await response.json() as T;
            } else {
                return {} as T;
            }
            
        } catch (error) {
            console.error(error);
            throw error;
        }
    }
```
The **executeRequest** function is where the magic happens. It takes the prepared request options, the specific endpoint, and any query parameters to construct the full URL. It then executes the fetch request, handling both success and error scenarios gracefully. For successful requests, it checks the response's content type and parses the JSON body accordingly, returning the parsed data. This method encapsulates error handling and response parsing, abstracting these concerns away from the consumer of our custom hook.

####Request Methods: Simplifying API Calls
With **prepareRequest** and **executeRequest** laying the groundwork, our hook exposes four methods for performing API calls: **get**, **post**, **put**, and **del**. Each method is tailored to its specific HTTP verb, simplifying the process of making requests. The consumer of the hook doesn't need to worry about the details of request preparation and execution, as these are handled internally:
```react
const get = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("GET", body);
        return executeRequest(request, endpoint, params);
    }

    const post = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("POST", body);
        return executeRequest(request, endpoint, params);
    }

    const put = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("PUT", body);
        return executeRequest(request, endpoint, params);
    }

    const del = async <T>(customRequestProps?: CustomRequestProps): Promise<T> => {
        const {body, endpoint, params} = customRequestProps ?? {};
        const request = await prepareRequest("DELETE", body);
        return executeRequest(request, endpoint, params);
    }
```
By abstracting the complexities of fetch API calls into these streamlined operations, developers can focus on the logic specific to their application, making API interactions more straightforward and less error-prone.

---

###Why Is It a Better Solution?
This abstraction not only cleans up our code, making it more readable and easier to follow, but it also significantly reduces the possibility of errors. By centralizing the request logic, we make our applications more robust and easier to test, improving development efficiency.

###Implementation in Action
With **useHttpClient** defined, we'll show how this can be consumed by another domain-specific custom hook, like **useTodolistService**, to interact with our to-do list API. This modular approach allows us to reuse **useHttpClient** across different contexts, keeping our code DRY and focused.
```react
export const useTodolistService = () => {
    const { get, post, put, del } = useHttpClient({endpoint: "/items"});

    const getAllItems = async (): Promise<Array<Item>> => {
        return get<Array<Item>>();
    } 

    const addItem = async (content: string): Promise<Item> => {
        const request: CustomRequestProps = {body: {content: content} as Item};
        return post<Item>(request);
    }

    const checkItem = async (id: number) => {
        const request: CustomRequestProps = {endpoint: `/${id}/check`};
        return put<void>(request);
    }

    const editContentItem = async (id: number, editedContent: string): Promise<Item> => {
        const request: CustomRequestProps = 
            {
                body: {content: editedContent} as Item,
                endpoint: `/${id}`, 
            };
        return put<Item>(request);
    }

    const deleteItem = async (id: number) => {
        const request: CustomRequestProps = {endpoint: `/${id}`};
        return del<void>(request);
    }

    return { getAllItems, addItem, checkItem, editContentItem, deleteItem };
}
```

####Consuming useTodolistService
```react
export const Todolist: FunctionComponent<TodolistProps> = () => {

    const [items, setItems] = useState<Array<Item>>([]);
    const { getAllItems } = useTodolistService();

    useEffect(() => {
        getAllItems()
            .then(items => setItems(items));
    },[]);
...
}
```

---

###Conclusion
In the end, we circle back to the initial problem and how our custom hook solution not only addresses these issues but does so efficiently. Now, with **useTodolistService**, consuming our hook in components becomes a trivial task, allowing us to manage state and perform CRUD operations with simplicity and elegance.

I appreciate you all reading the entire article. I hope it has been useful to you and has provided you with a new tool for your projects. I leave you the [repository](https://github.com/JerezM/integrating-react-router-v6) here if you want to get the code used in this post.

In the next post I will show how I successfully integrated **Auth0** into my app from the frontend to the backend, and I also will show how I added authorization headers using the custom hook explained in this post. 
To find out when the next post is published, I invite you to follow me on my [Dev.to](https://dev.to/jerezm) profile or [LinkedIn](https://www.linkedin.com/in/jerez-martin/). 
I will announce there when it is available.

See you next time!