<BAM type='blog'/>
Blog Posts

Building a React Custom Hook to Fetch Data using Axios - 2023-01-03

Developing a reusable React custom hook to fetch API data

A few days ago I found myself writing the same fetch code over and over again across a few components. So, I went ahead and took a look at some older components to reevaluate how we are fetching data and it looks like I need to refactor.

After looking into some possible solutions, I decided on development my own hook to interact with the API.

So you don't have to scroll.

Here is a link to my repo

Here is a link to Reacts documentation about custom hooks.

This will be using the NextJS and Material UI template

Material UI's NextJs template

Additionally I will be using Axios to fetch the data.

Axios NPN Page

Table of Contents

Fetching Data with useEffect

As I stated before I found myself writing the following code over and over again across components that needed this data.

Quick explanation:

The useEffect method is called on the first render of the component. Which then executes an asynchronous method to fetch the data.

The "methodToFetchData" function then calls a service method to get the data.

We then handle the promise object using the .then annotation and either pass the data to be handled or set into state;

I usually extract the fetch logic to a separate method so I can reuse it incase I need to call the data after updates.

Note: There is usually a little more going on here, like setting a loading state variable to display a loading screen for the user.

The "regular" way performs all the actions in the useEffect method.

// Asynchronous Way useEffect(() => { const fetchData = async () => { await methodToFetchData(); }; fetchData(); }, []) const methodToFetchData = async () => { setIsLoading(true) DataService.get() .then(response => { /* * Option 1: This method would prepare the data as I needed and set it into state * Option 2: If I didn't need anything special, I save it to state directly */ //Option 1: await handleResponse(response.data); //Option 2: setData(response.data) setIsLoading(false) }) .catch(e) => { handleError(e); } .finally(()=>{ setIsLoading(false); }) } // Regular Way useEffect(() => { setIsLoading(true) DataService.get() .then(response => { setData(response.data) } .catch(e) => { handleError(e); } .finally(()=>{ setIsLoading(false); }) }, [])

useData.js Implementation

In the example below and in my repo I will be using a testing API. Which you can find in the link below

https://jsonplaceholder.typicode.com/guide/

The first thing I like to do is separate the Axios configuration creation, which holds the baseUrl to the API and any special headers the endpoints may require.

I call this file http-axios.js and import it into my file for use.

import axios from "axios"; export default axios.create({ //Testing API for this example baseURL: "https://jsonplaceholder.typicode.com", headers: { "Content-type": "application/json; charset=UTF-8", }, });

Below I will go into the hook in detail but if you want to skip that here is a link to the file

Github - useData.js

Imports

Of course the standard React imports!

http: the configuration file we created with the axios defaults.

State Variables

isLoading: boolean state to used to indicate when the data is still loading.

returnData: the data fetched from the API.

error: any error message the api call could have encountered.

import React, { useState, useEffect } from "react"; import http from "./services/http-axios"; export default function useData(url) { const [isLoading, setIsLoading] = useState(false); const [returnData, setReturnData] = useState([]); const [error, setServerError] = useState(null); ...

Methods - Initial Load

useEffect: On the initial load, we want to fetch the data.

getData: The method that gets the API data and assigns it to state for consumption. It also assigns the isLoading variable.

useEffect(() => { if (!url) return; const fetchData = async () => { getData(url); }; fetchData(); }, [url]); const getData = async (url) => { setIsLoading(true); const response = await http .get(url) .then((response) => { setReturnData(response.data); }) .catch((error) => { setServerError(error); }) .finally(() => setIsLoading(false)); };

Methods - CRUD

The following methods seem pretty self explanatory.

const createData = async (url, post) => { if (!url) return; setIsLoading(true); const response = await http .post(url, post) .then((response) => { setReturnData(updatedData); }) .catch((error) => { setServerError(error); }) .finally(() => setIsLoading(false)); }; const updateData = (url, post) => { if (!url) return; setIsLoading(true); const response = http .put(url, post) .then((response) => { setReturnData(returnData); }) .catch((error) => { setServerError(error); }) .finally(() => setIsLoading(false)); }; const deleteData = async (url, postId) => { if (!url) return; setIsLoading(true); const response = await http .delete(url) .then((response) => { setReturnData(filteredArray); }) .catch((error) => { setServerError(error); }) .finally(() => setIsLoading(false)); };

Exporting methods and variables

We now have to return all the methods and variables we need to use.

return { isLoading, returnData, error, getData, createData, updateData, deleteData, handleMockedPost, };

Using the Hook to fetch data

You can see here, we are destructuring all the variables and methods from the hook to consume and use.

In this example we pass the API url to the hook in order for it to do its work.

const { isLoading, returnData, error, getData, createData, updateData, deleteData, } = useData("posts");

We can now use the isLoading & error variables to render a loading or error message to the use while the data is fetching

{error ? <p>Error fetching data {error}</p> : null} {isLoading ? ( <p>Data Loading...</p> ) : ( ...rest of the component )

Now that we have data, we can use the returnData variable as we would. Here I iterate through it and display it in a table.

Each row has a edit an delete icon, which calls the appropriate method in the hook.

I recommend taking a look at the repo code to see how I mocked the API calls, since the API doesn't write to a database.

<TableContainer component={Paper}> <Table sx={{ maxWidth: "90%" }} aria-label="simple table"> <TableHead> <TableRow> <TableCell>Post Id</TableCell> <TableCell>User Id</TableCell> <TableCell>Title</TableCell> <TableCell>Body</TableCell> <TableCell></TableCell> </TableRow> </TableHead> <TableBody> {returnData.map((row) => ( <TableRow hover key={row.id} sx={{ "&:last-child td, &:last-child th": { border: 0 }, }} > <TableCell component="th" scope="row"> {row.id} </TableCell> <TableCell>{row.userId}</TableCell> <TableCell>{row.title}</TableCell> <TableCell>{row.body}</TableCell> <TableCell onClick={() => handleUpdate(row)}> <ModeEditOutlineOutlinedIcon fontSize="large" sx={{ color: "blue" }} /> </TableCell> <TableCell onClick={() => handleRemove(row)}> <DeleteForeverIcon fontSize="large" sx={{ color: "red" }} /> </TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer>

Conclusion

After some research and consideration, this was my approach to having to rewrite the same code over and over again.

I tried sticking with Reacts Best Practices in regards to Custom Hooks, and stick to what others have done. So when I come back to it later I don't go crazy!

I am sure I will be tweaking this over the next several weeks as I find spots were it doesn't quite fit in my code base (I hope not). Additionally, i know there is always room for improvements and use of features I don't haven't looked into yet.

Buy Me A Coffee