Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

Building Custom Hooks for Stateful Logic

Recommended resources

Hello! Welcome back to our module on handling side effects in React.

In our last lesson, you mastered the useEffect cleanup function, making our data-fetching logic robust and leak-proof by using an AbortController. We ended with a question: while our fetching logic is now complete (handling loading, errors, and cleanup), it's a lot of code to repeat in every component that needs data.

Today, we'll solve that problem. Your learning outcome is to create a reusable custom hook to encapsulate stateful logic, such as data fetching. This is a fundamental pattern in modern React development that promotes clean, reusable, and maintainable code—principles that are just as important on the front-end as they are in backend development with frameworks like Laravel.

1. What Are Custom Hooks?

At their core, custom hooks are just JavaScript functions that follow two rules:

  1. Their name must start with the word use (e.g., useFetch, useLocalStorage). This is a convention that allows React's tooling to enforce hook rules.
  2. They can call other hooks (like useState, useEffect, or even other custom hooks).

By extracting component logic into a custom hook, you can hide the complex implementation details and reuse that logic across your entire application.

Creating Custom Hooks for API Calls

This article from NamasteDev provides an excellent introduction to why we use custom hooks, especially for a common task like API calls.

Please read the sections 'What Are Custom Hooks?' and 'Why Use Custom Hooks for API Calls?'. Focus on how they help encapsulate logic for data fetching, error handling, and loading states.

As the article explains, the goal is to follow the DRY (Don't Repeat Yourself) principle. Instead of writing the same useState and useEffect boilerplate over and over, we'll write it once inside a custom hook.


2. Refactoring to a useFetch Hook

Let's take the complete and robust data-fetching logic we created in the last lesson and refactor it into a custom hook called useFetch.

The "Before" State

Recall the final version of our data-fetching component. It manages its own data, loading, and error states, and includes the AbortController for cleanup.

// In a component like PokemonViewer.js
function PokemonViewer() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 1. Create an AbortController for this effect
    const controller = new AbortController();

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch("https://pokeapi.co/api/v2/pokemon/ditto", {
          // 2. Pass the signal to the fetch request
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (e) {
        // Check if the error was due to aborting
        if (e.name !== 'AbortError') {
          setError(e);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // 3. Return a cleanup function that aborts the controller
    return () => {
      controller.abort();
    };
  }, []); // The effect runs once on mount

  // ... JSX to render loading, error, or data
}

This is a lot of logic to have inside a single component. Now, let's extract it.

Creating and Using the useFetch Hook

The process involves moving the logic into a new function and making it generic.

React Custom Hook - useFetch

This video from TechCheck provides a great visual walkthrough of this exact refactoring process. It demonstrates how to create the hook file, move the logic, and use it back in the component.

Watch the video from the beginning (00:00) to 00:33, then skip to 01:39 and watch until 06:03. Pay attention to these key steps: Why create a custom hook (0:00 - 0:33) Creating the useFetch.js file (1:39 - 2:37) Making it generic by passing a url and renaming state (2:37 - 4:25) Returning values in an object for destructuring (4:25 - 4:56) Using the hook in the component (4:56 - 6:03) Note: The video uses axios and a different cleanup method, but the core refactoring principles are identical to what we are doing with fetch and AbortController.

Following the steps shown in the video, here's how we create our own useFetch hook.

Step 1: Create the hook file
In your src directory, create a new folder called hooks. Inside it, create a file named useFetch.js.

Step 2: Define the hook and move the logic
We'll move all the state management and effect logic from our component into this new function. We'll also make it generic by accepting a url as an argument.

// src/hooks/useFetch.js

import { useState, useEffect } from 'react';

// The function name starts with "use"
const useFetch = (url) => {
  // All the state we need is now managed inside the hook
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url, { // Use the url argument
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (e) {
        if (e.name !== 'AbortError') {
          setError(e);
        }
      } finally {
        // We only set loading to false if the request wasn't aborted
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      controller.abort();
    };
  }, [url]); // The effect now re-runs if the URL changes

  // The hook returns an object with the state values
  return { data, loading, error };
};

export default useFetch;

Step 3: Use the hook in the component (The "After" State)
Now, our PokemonViewer component becomes incredibly simple and readable. All the complex logic is hidden away in the useFetch hook.

// In a component like PokemonViewer.js

import React from 'react';
import useFetch from '../hooks/useFetch'; // Import our new hook

function PokemonViewer() {
  // One line replaces all the state and effect logic!
  const { data: pokemon, loading, error } = useFetch("https://pokeapi.co/api/v2/pokemon/ditto");

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>{pokemon?.name}</h1>
      <img src={pokemon?.sprites?.front_default} alt={pokemon?.name} />
    </div>
  );
}

Notice a few things here:

  • We replaced all the useState and useEffect calls with a single line.
  • We are using object destructuring to get data, loading, and error from our hook.
  • We can even rename the data variable to something more descriptive (like pokemon) right in the destructuring assignment: const { data: pokemon, ... }.

3. Custom Hooks for Any Stateful Logic

Data fetching is a classic use case, but custom hooks are a general-purpose tool. You can encapsulate any piece of stateful logic you find yourself reusing. A common example is interacting with browser APIs like localStorage.

Learn Custom Hooks In 10 Minutes

To see another powerful example, this video from Web Dev Simplified demonstrates building a useLocalStorage hook. It's a fantastic illustration of how hooks can abstract away complexity.

Watch the segment from 01:27 to 07:42. Notice how he creates a hook that has the same interface as useState but secretly persists the data to localStorage under the hood. This is the power of abstraction that custom hooks provide.

The key idea is that if you have a chunk of logic involving useState and/or useEffect that you might need in more than one place, it's a prime candidate for a custom hook.


Conclusion

You have now learned one of the most powerful patterns in the React ecosystem. By creating custom hooks, you can build your own "library" of reusable, stateful logic, making your components cleaner, more declarative, and easier to maintain.

Key Takeaways:

  • Custom hooks are reusable functions whose names start with use and which can call other hooks.
  • They are the primary way to share stateful logic between components in React.
  • The useFetch hook is a common and powerful pattern that encapsulates data fetching, loading, error handling, and cleanup logic.
  • Using a custom hook dramatically simplifies the components that consume it, hiding implementation details and improving readability.

Preview of the Next Lesson:
You've now learned all the individual pieces for handling data and side effects: useEffect for the fetch itself, state management for loading/error states, cleanup functions for safety, and custom hooks for reusability. In our next lesson, you will put it all together and build a mini-app that fetches and filters items from an API using these hooks. This will be the capstone project for this module.