If you are not already familiar with the process of fetching data from an API or with React Hooks, I recommend checking these articles on those topics:
- Fetching Data in React: https://upmostly.com/tutorials/react-how-to-fetch-data-from-api
- Introduction to React Hooks: https://upmostly.com/tutorials/react-hooks-simple-introduction
Now that we’re on the same page in terms of familiarity with the concept of hooks and data retrieval, we can discuss about the implementation of our very own custom hook, designed specifically for fetching data from an external API.
Table of Contents
Why should you implement a Custom React Hook to Fetch Data?
As with custom hooks in general, the main benefits of using them are:
- Readability
- The ability to encapsulate side effects
- Reuse logic across multiple components
But apart from those general advantages, a custom hook implementation for fetching data also provides the benefit of having a singular, controlled way to handle requests state such as when a request has been fulfilled successfully (The data has been retrieved), the request is in loading state (Have a baseline loading state visual hint – Loader), or when the request has failed (Always log the error in the console).
This benefit is extremely important for providing an intuitive user experience by letting the user know that whenever something is loading there will always be a “Loading…” message shown, or a specific Loader animation in place, rather than having different ways of handling this state in different areas of the application (Unless that is required, then you can always pass certain arguments to the hook in order to overload the behavior).
Practical Data Fetching Hook Example
Let’s say that we’re building a Blog application, similar to the one you’re reading this article on. We would have a lot of interaction with articles in various parts of the application, with many articles sections having a similar design and similar behavior.
If you were to build it without using a hook for fetching data, those sections would look something like this:
function App() {
const [requestState, setRequestState] = useState({
data: [],
loading: true,
failed: false,
});
useEffect(() => {
const retrieveArticles = async () => {
try {
const articles = await axios.get("our-endpoint");
setRequestState((prevState) => ({
...prevState,
data: articles,
}));
} catch (err) {
setRequestState((prevState) => ({
...prevState,
failed: true,
}));
} finally {
setRequestState((prevState) => ({
...prevState,
loading: false,
}));
}
};
retrieveArticles();
}, []);
return (
<div className="App">
{requestState.loading ? (
<p>Data is currently loading...</p>
) : requestState.failed ? (
<p>There was an issue loading the articles.</p>
) : (
requestState.data.map((article) => (
<Article title={article.title} body={article.body} />
))
)}
</div>
);
}
As you can see in the example above, it’s a rather straightforward implementation of a way to fetch data in React. However, what happens if we want to make the exact same request, or use the same config but with a different URL?
Well, you’ve guessed it! We’ll have to do it all over again, which is not really the most convenient way of dealing with this scenario, especially if the application will grow massively in the future.
So, how do we ease the process? That’s right, hooks will now come into play. Let us see the reproduced example with hooks below:
import axios from "axios";
import { useEffect, useState } from "react";
const useFetchData = (
requestConfig, // Axios request config
) => {
const localRequestConfig = requestConfig || {};
const [state, setState] = useState({
loading: true,
data: null,
error: null,
});
if (!localRequestConfig?.method) {
localRequestConfig.method = 'GET';
}
useEffect(() => {
if (localRequestConfig.url) {
axios(localRequestConfig)
.then((res) => {
setState(prevState => ({
...prevState,
data: res.data,
}))
})
.catch((err) => {
setState(prevState => ({
...prevState,
error: err,
}))
})
.finally(() => {
setState(prevState => ({
...prevState,
loading: false,
}))
});
} else {
setState(prevState => ({
...prevState,
loading: false,
error: new Error('No URL provided!'),
}));
}
return state;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [requestConfig]);
};
export default useFetchData;
Now that we’ve created the hook, let’s also replace the previous implementation and add it to our application:
function App() {
const {
data: articles,
loading: articlesLoading,
error: articlesError,
} = useFetchData();
return (
<div className="App">
{articlesLoading ? (
<p>Data is currently loading...</p>
) : articlesError ? (
<p>There was an issue loading the articles.</p>
) : (
articles.map((article) => (
<Article title={article.title} body={article.body} />
))
)}
</div>
);
}
So, as you can see, we are now working with a much better way of reutilizing the API call logic with no missing functionality. By providing the requestConfig parameter to the hook we are improving reusability much further, as the user can simply pass the configuration that he needs to work with and that’s all. No further headaches; it’s simply plug-and-play.
Now if we were to need the API call logic in 10 other components we wouldn’t need to copy all of this chunk here:
const [requestState, setRequestState] = useState({
data: [],
loading: true,
failed: false,
});
useEffect(() => {
const retrieveArticles = async () => {
try {
const articles = await axios.get("our-endpoint");
setRequestState((prevState) => ({
...prevState,
data: articles,
}));
} catch (err) {
setRequestState((prevState) => ({
...prevState,
failed: true,
}));
} finally {
setRequestState((prevState) => ({
...prevState,
loading: false,
}));
}
};
retrieveArticles();
}, []);
This snippet would be all we need to copy over:
const {
data: articles,
loading: articlesLoading,
error: articlesError,
} = useFetchData();
Voila! Now that we’ve implemented a hook to handle the data fetching process we can reuse it wherever we like by just copying over 5 lines of code. Of course, if you do need to extend that functionality to handle some other scenarios you could simply parameterize the hook so that you won’t run into any insufficiencies.
Just so you can get a better perspective, we’ve managed to reduce the component file’s size by this much:
A good next step after refactoring your codebase by using hooks would be to cache your requests in order to save up resources and improve performance. You can read my other article helping you understand how here: https://upmostly.com/tutorials/how-to-integrate-cache-in-react-applications
Final Word
I hope you have learned something new by reading this article, and hope you have enjoyed it. Catch you on the next one!
Also, if you have any suggestions, questions, or tips to share, feel free to leave a comment below. That would be highly appreciated.
Cheers!
💬 Leave a comment