Skip to main content

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:

  1. Show a loading indicator while the data is being fetched.
  2. Display an error message if the fetch fails.
  3. 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:

  1. const controller = new AbortController();: We create a new AbortController for each fetch request.
  2. const signal = controller.signal;: We get the signal from the controller.
  3. fetch(url, { signal }): We pass the signal to the fetch request.
  4. if (error.name === 'AbortError'): In our catch 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.
  5. return () => { controller.abort(); };: The useEffect hook returns a cleanup function. This function will be called when the component unmounts or when the url changes. In it, we call controller.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.

Further Reading