A Custom Hook for Data Fetching: `useFetch` (Part 2) - Adding Loading and Error States #85
📖 Introduction
Following our creation of a basic useFetch
hook, this article explores how to make it more robust and production-ready. We will add proper loading and error states, and handle request cancellation to prevent common issues like memory leaks.
📚 Prerequisites
Before we begin, please ensure you have a solid grasp of the following concepts:
- All concepts from Part 1 of this series.
- The
AbortController
API for canceling fetch requests.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ The Problem: Understanding the need for loading and error states, and the issue of race conditions and memory leaks.
- ✅ Core Implementation: How to add loading and error states to the
useFetch
hook. - ✅ Advanced Techniques: How to handle request cancellation using the
AbortController
API.
🧠 Section 1: The Core Concepts of a Robust useFetch
Hook
Our basic useFetch
hook is a good start, but in a real-world application, we need to handle more than just the happy path. We need to:
- Show a loading indicator while the data is being fetched.
- Display an error message if the fetch fails.
- Cancel the request if the component unmounts before the fetch is complete. This prevents memory leaks and race conditions.
To handle request cancellation, we will use the AbortController
API, which is the modern standard for canceling fetch requests.
💻 Section 2: Deep Dive - Implementation and Walkthrough
Let's enhance our useFetch
hook.
2.1 - The Enhanced useFetch
Hook
// useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
setError(null);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(error);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => {
controller.abort();
};
}, [url]);
return { data, loading, error };
}
export default useFetch;
Step-by-Step Code Breakdown:
const controller = new AbortController();
: We create a newAbortController
for each fetch request.const signal = controller.signal;
: We get thesignal
from the controller.fetch(url, { signal })
: We pass thesignal
to thefetch
request.if (error.name === 'AbortError')
: In ourcatch
block, we check if the error was caused by an abort. If so, we log a message to the console. Otherwise, we set the error state.return () => { controller.abort(); };
: TheuseEffect
hook returns a cleanup function. This function will be called when the component unmounts or when theurl
changes. In it, we callcontroller.abort()
to cancel the fetch request.
🛠️ Section 3: Project-Based Example: A Component with Loading and Error States
Now, let's use our enhanced useFetch
hook in a component.
// Post.js
import React from 'react';
import useFetch from './useFetch';
function Post({ postId }) {
const { data: post, loading, error } = useFetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
if (loading) {
return <div>Loading post...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
export default Post;
This component will now correctly display loading and error states, and it will cancel the fetch request if the postId
prop changes before the fetch is complete.
💡 Conclusion & Key Takeaways
In this article, we've made our useFetch
hook much more robust. By adding loading and error states and handling request cancellation, we've created a hook that is ready for use in production applications.
Let's summarize the key takeaways:
- A robust
useFetch
hook should manage loading and error states. - Request cancellation is important for preventing memory leaks and race conditions.
- The
AbortController
API is the modern standard for canceling fetch requests.
Challenge Yourself:
To solidify your understanding, try to create a component that has a "Next Post" button that increments the postId
and fetches the next post. Observe how the useFetch
hook cancels the previous request when a new one is initiated.
➡️ Next Steps
You now have a solid understanding of how to create a robust and reusable useFetch
hook. In the next article, "A Custom Hook for Local Storage: useLocalStorage
(Part 1)", we will explore how to create a custom hook for persisting state in the browser's local storage.
Thank you for your dedication. Stay curious, and happy coding!
glossary
AbortController
: A web API that allows you to abort one or more web requests as desired.- Race Condition: A situation where the behavior of a system depends on the sequence or timing of other uncontrollable events.
- Memory Leak: A type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.