Hello! Welcome back to our module on working with APIs in React.
In our last few lessons, you've become proficient at fetching data with GET requests and sending data with POST and PUT requests. You might have noticed a pattern emerging: in any component that needs to fetch data, we end up writing very similar code:
- Three
useStatehooks fordata,isLoading, anderror. - A
useEffecthook to run thefetchcall. - Logic inside the effect to set loading states and handle success or error cases.
If we need to fetch data in five different components, we'd be repeating this logic five times. This violates the "Don't Repeat Yourself" (DRY) principle and makes our code harder to maintain.
Today, we'll solve this problem by learning how to refactor data-fetching logic into a reusable useFetch custom hook. Custom hooks are one of the most powerful features in React, allowing you to extract and share stateful logic between components. By the end of this lesson, you'll be able to create a clean, reusable hook that encapsulates all the complexity of data fetching, making your components much simpler and more declarative.
The Problem: Code Duplication
Imagine you have a Home component that fetches a list of blog posts and another component, say UserProfile, that fetches user details. Both would contain nearly identical useEffect blocks. If you need to change how you handle loading states or errors, you'd have to update it in both places.
This is exactly the problem custom hooks were designed to solve.
Full React Tutorial #20 - Making a Custom Hook
To start, let's watch this segment from Net Ninja that clearly explains why this repetitive logic is a problem and introduces the idea of creating a custom hook to make the code more reusable and maintainable.
Watch from the beginning to 01:38. The video will set the stage by showing data-fetching logic inside a component and explaining the drawback of this approach.
Creating Your First Custom Hook: useFetch
A custom hook is simply a JavaScript function whose name starts with use and that can call other hooks like useState or useEffect. Let's walk through the process of creating our useFetch hook.
The core idea is to move all the data-fetching logic (the state and the effect) out of the component and into our new useFetch function. This function will then provide the data, loading status, and error back to any component that uses it.
The following video provides a complete, step-by-step walkthrough of this process. It covers:
- Creating the
useFetch.jsfile and the basic function. - Moving the
useStateanduseEffectlogic into the new hook. - Making the hook generic by accepting a
urlas an argument. - Returning the state values from the hook.
Full React Tutorial #20 - Making a Custom Hook
Now, let's build the hook. Follow along as Net Ninja refactors the component's logic into a useFetch hook.
Watch from 01:38 to 06:32. Pay close attention to each step of the transformation: Creating the function (01:38): Notice the use naming convention. Moving the logic (02:23): See how the existing useState and useEffect calls are simply cut and pasted into the hook. Generalizing the hook (03:22): Observe how hardcoded names like blogs are changed to data, and the URL is passed in as a parameter (url). Returning values (04:43): The hook returns an object containing the three pieces of state. Passing the URL (05:39): The url parameter is used in the fetch call and added to the useEffect dependency array.
Using the useFetch Hook in a Component
Once the useFetch hook is created and exported, using it is incredibly simple. You can remove all the useState and useEffect logic related to data fetching from your component and replace it with a single line of code.
This makes your component much cleaner. It no longer needs to know how the data is fetched; it just needs to know that it's getting data, a loading status, and a potential error from our hook.
Full React Tutorial #20 - Making a Custom Hook
Let's see the final result. This last clip shows how to import and use the newly created useFetch hook in the Home component.
Watch from 06:32 to the end. Notice how the component is simplified. The video also shows a neat trick for renaming destructured variables (e.g., const { data: blogs, ... }), which is useful when the generic name data isn't descriptive enough in your component.
Here is a summary of the code transformation:
Before (in Home.js):
import { useState, useEffect } from 'react';
const Home = () => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('http://localhost:8000/blogs')
.then(res => {
if (!res.ok) {
throw Error('Could not fetch the data for that resource');
}
return res.json();
})
.then(data => {
setIsLoading(false);
setData(data);
setError(null);
})
.catch(err => {
setIsLoading(false);
setError(err.message);
})
}, []);
// ... JSX to render data, loading, or error ...
};
After (using the hook):
// In useFetch.js
import { useState, useEffect } from 'react';
const useFetch = (url) => {
// ... all the useState and useEffect logic is here ...
return { data, isLoading, error };
};
export default useFetch;
// In Home.js
import useFetch from './useFetch';
const Home = () => {
const { data: blogs, isLoading, error } = useFetch('http://localhost:8000/blogs');
// ... JSX to render data, loading, or error ...
};
Look at how much cleaner the Home component has become!
Preventing Errors on Unmounted Components
There's one more crucial improvement we should make. What happens if a user navigates away from a component while our useFetch hook is still fetching data? When the fetch call completes, it will try to call setData, setIsLoading, etc., on a component that is no longer in the DOM. This causes a common React warning: "Can't perform a React state update on an unmounted component."
To fix this, we can use the cleanup function that useEffect provides. The function you return from useEffect is executed when the component unmounts. We can use this to set a flag that tells our fetch promise to ignore the result if the component is gone.
This article provides an excellent, detailed guide to building a robust data-fetching hook. We will focus on the two most relevant sections.
How to fetch data with React Hooks
First, let's read a section that reinforces what you just saw in the video, but in a text format. This will solidify your understanding of the refactoring process.
Read the section titled "Data Fetching with Custom Hook". It walks through the same process of extracting useState and useEffect into a custom hook, which will help reinforce the pattern.
Now, let's address the unmounted component problem.
How to fetch data with React Hooks
Next, read the following section in the same article. It explains how to use a cleanup function within useEffect to prevent state updates after a component has unmounted.
Read the section titled "Abort Data Fetching with React Hooks". Focus on understanding the didCancel boolean flag and how the returned cleanup function (return () => { didCancel = true; };) works to prevent the state update.
By adding this cleanup logic, your useFetch hook becomes much more robust and safe to use across your entire application.
Conclusion
Congratulations! You've just unlocked one of React's most elegant patterns for code reuse. By abstracting repetitive logic into custom hooks, you can build cleaner, more maintainable, and more testable applications. The useFetch hook is a classic and incredibly useful example that you'll likely use in almost every project.
Key Takeaways:
- Custom hooks are reusable functions starting with
usethat let you share stateful logic between components. - The primary motivation for creating a custom hook is to avoid duplicating logic (DRY principle).
- To create a
useFetchhook, you move theuseStatecalls fordata,loading, anderror, along with theuseEffectcall for the fetch request, into a new function. - The hook should be made generic by accepting parameters like the
url. - It should return the state values needed by the component, typically as an object:
{ data, isLoading, error }. - Implementing a cleanup function in
useEffectis crucial for preventing memory leak warnings when a component unmounts during a pending API request.
Next Up:
The didCancel flag we just implemented prevents state updates, but it doesn't actually stop the network request. The request still completes in the background, consuming bandwidth. In our next lesson, we will explore a more advanced technique to handle API request cancellation using the AbortController API, which truly cancels the fetch request in flight.