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.
This will be using the NextJS and Material UI template
Material UI's NextJs templateAdditionally I will be using Axios to fetch the data.
Axios NPN PageFetching 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.jsImports
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.
