react-custom-hooks

How to Create Custom Hooks in React

  • 5 min

So far, we’ve been using the Hooks that React gives us “out of the box”. For example, useState for state and useEffect for side effects.

But what happens when we have complex logic that we need to use in several components at once?

In React, just as we extract repeated UI into Components, sometimes we extract repeated logic into Custom Hooks

A Custom Hook is a JavaScript function that uses other Hooks (like useState, useEffect, etc.) to encapsulate reusable logic.

The main idea is to extract logic from a component and place it in a separate function, which can then be used in multiple components.

When to Create a Custom Hook?

Custom Hooks are a very useful feature of React. They allow us to build our own “framework” of utilities tailored to the specific needs of our project. But don’t go overboard.

My recommendation is the “Repeat twice, then refactor” rule.

  1. The first time, write the logic inside the component
  2. The second time you need the same thing elsewhere, copy and paste it
  3. The third time, it’s the mandatory moment to refactor and extract into a Custom Hook

What is a Custom Hook?

A Custom Hook is, in reality, a simple JavaScript function. It’s a function that can contain logic, calculations, and, most importantly, can call other React Hooks inside it.

There is one strict rule: your function’s name MUST start with “use”.

  • useCounter
  • useForm
  • useFetch
  • createCounter

This isn’t just a convention for us humans to read better. React’s Linter looks for functions starting with use to verify that you follow the rules of hooks (for example, not calling them inside conditionals).

Example 1: Abstracting State (useCounter)

Let’s see it better with an example. We’ll create our first Custom Hook. Time to make a counter again! (yeah… sorry about that 😎)

As we know, the counter is the “Hello World” of reactivity. This time, we want to encapsulate the logic of a number that goes up, down, and resets.

Create a new file, for example /src/hooks/useCounter.js:

import { useState } from "react";

// 1. Define the function with the 'use' prefix
export const useCounter = (initialValue = 0) => {
  const [counter, setCounter] = useState(initialValue);

  // 2. Define the manipulation functions
  const increment = (step = 1) => setCounter(c => c + step);
  const decrement = (step = 1) => setCounter(c => c - step);
  const reset = () => setCounter(initialValue);

  // 3. Return what the component needs
  // It can be an array [] or an object {}
  return {
    counter,
    increment,
    decrement,
    reset,
  };
};
Copied!

How do we use it?

Now, in our components, the code becomes very clean. We no longer care about how the counter is incremented, we just use the functionality.

import { useCounter } from "./hooks/useCounter";

function Cart() {
  // Initialize the hook
  const { counter, increment, decrement } = useCounter(1);

  return (
    <div>
      <h3>Products: {counter}</h3>
      <button onClick={() => decrement()}>-</button>
      <button onClick={() => increment()}>+</button>
    </div>
  );
}
Copied!

If tomorrow we want to change the logic (for example, so the counter never goes below zero), we only modify useCounter.js and all components will be updated.

Sharing Logic vs Sharing State

When we use a Custom Hook in two different components, they are not sharing state. They are sharing the state logic.

// Component A
const counterA = useCounter(); // Has its own internal state (0)

// Component B
const counterB = useCounter(); // Has ITS OWN internal state (0)
Copied!

That is, if I increment counterA, counterB doesn’t know about it.

Each call to a Custom Hook creates a completely isolated instance of the states (useState) and effects (useEffect) it contains inside.

If what you want is to share state globally (so that modifying one changes both), you need other tools like Context API or global state managers (Zustand, Redux), which we’ll see later.

Example 2: Abstracting Effects (useFetch)

Let’s look at another more advanced example, one that isn’t a counter. Let’s encapsulate the complexity of useEffect, async requests, and error handling in a reusable hook.

The goal is to be able to fetch data as easily as this: const { data, loading, error } = useFetch(url);

So we create our file src/hooks/useFetch.js with our Custom Hook.

import { useState, useEffect } from "react";

export const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Reset states when the URL changes
    setLoading(true);
    setData(null);
    setError(null);
    
    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then((response) => {
        if (!response.ok) throw new Error("Request error");
        return response.json();
      })
      .then((jsonData) => {
        setData(jsonData);
        setError(null);
      })
      .catch((err) => {
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      })
      .finally(() => {
        setLoading(false);
      });

    // Cleanup: Cancel the request if the component unmounts
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
};
Copied!

Usage in Components

Look at the difference in our main component. We’ve gone from having 20 lines of async logic to a single declarative line.

function UserList() {
  const { data, loading, error } = useFetch("https://api.example.com/users");

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

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
Copied!