Custom Hooks

Custom hooks are just functions that call other hooks -- that one rule unlocks the ability to extract fetch logic, timers, and subscriptions into clean, reusable units.

May 15, 20264 min read5 / 7

After adding StrictMode and React DevTools, the development environment is solid. Now there is a piece of logic worth extracting: fetching the pizza of the day.

The WET Rule

There is a common refactoring heuristic: DRY, or Don't Repeat Yourself. The version of this that applies specifically to hooks is WET -- Write Everything Twice. Not once, not three times. Twice.

The reasoning: the first time you write something, you are still figuring out the shape of the problem. Abstracting too early locks in an API before you understand what it needs to be. The second time you write it, the pattern is clear. The third time is when you extract it.

For the pizzeria app, usePizzaOfTheDay gets extracted not because the code is repeated but because it is genuinely a self-contained unit of behavior -- fetching a thing, managing loading state, returning the result. That combination is worth naming.

Writing the Custom Hook

Create src/usePizzaOfTheDay.js:

JSX
import { useState, useEffect } from "react"; const usePizzaOfTheDay = () => { const [pizzaOfTheDay, setPizzaOfTheDay] = useState(null); useEffect(() => { fetch("/api/pizza-of-the-day") .then((res) => res.json()) .then((data) => setPizzaOfTheDay(data)); }, []); return pizzaOfTheDay; }; export default usePizzaOfTheDay;

A custom hook is any function that:

  1. Has a name starting with use
  2. Calls at least one React hook internally

That is the entire definition. There is no special API to implement, no class to extend, no registration step. The use prefix is a convention that tells both developers and the linter that this function follows the rules of hooks.

Using the Hook in a Component

JSX
import usePizzaOfTheDay from "./usePizzaOfTheDay"; const PizzaOfTheDay = () => { const pizzaOfTheDay = usePizzaOfTheDay(); if (!pizzaOfTheDay) { return <div>Loading pizza of the day...</div>; } return ( <div className="pizza-of-the-day"> <h2>Pizza of the Day</h2> <img src={pizzaOfTheDay.image} alt={pizzaOfTheDay.name} /> <h3>{pizzaOfTheDay.name}</h3> <p>{pizzaOfTheDay.description}</p> </div> ); }; export default PizzaOfTheDay;

The component has no idea that a fetch is happening. It calls usePizzaOfTheDay(), gets back either null or a pizza object, and renders accordingly. The fetch logic, the loading state, and the effect are all inside the hook.

Each Instance Gets Its Own State

Custom hooks do not share state between calls. If two components both call usePizzaOfTheDay(), each component gets its own pizzaOfTheDay state variable and its own effect. They run independently. To share state between components, you need either a parent component holding the state, or context.

Custom hook: each component call gets its own isolated state and effect instance ExpandCustom hook: each component call gets its own isolated state and effect instance

When to Extract vs When to Leave It Inline

A custom hook is worth creating when:

  • The combination of state + effect appears more than twice
  • The behavior has a clear, nameable identity (usePizzaOfTheDay, useWindowSize, useLocalStorage)
  • Tests need to cover the logic independently from the component

Leave it inline when it is used in one place and the component reads clearly without the extraction.

useDebugValue

There is one hook that exists purely for DevTools: useDebugValue. It has no effect on rendering. It attaches a label to your custom hook so it is identifiable in the React DevTools Components tab.

JSX
import { useState, useEffect, useDebugValue } from "react"; const usePizzaOfTheDay = () => { const [pizzaOfTheDay, setPizzaOfTheDay] = useState(null); useDebugValue( pizzaOfTheDay ? `${pizzaOfTheDay.id}: ${pizzaOfTheDay.name}` : "loading" ); useEffect(() => { fetch("/api/pizza-of-the-day") .then((res) => res.json()) .then((data) => setPizzaOfTheDay(data)); }, []); return pizzaOfTheDay; };

In the Components tab, next to the usePizzaOfTheDay hook entry, DevTools will show the string you passed -- something like "42: Green Garden". Without it, you would just see the hook name. With it, you can tell at a glance what the hook is holding.

This is most useful when a codebase has many custom hooks and you need to distinguish them from each other in the panel. In a smaller app, it is optional.

The pizzeria app still needs a cart. The next post wires up form submission and builds the Cart component -- the first time user input actually changes what shows up in the UI.

The Essentials

  1. A custom hook is any function that starts with use and calls at least one React hook. No special API, no registration. The naming convention is what enables linter rules and makes intent clear.
  2. Each call to a custom hook gets its own state. Two components calling usePizzaOfTheDay() each get independent state and effects.
  3. Extract when the logic has a clear identity and appears more than twice. The WET rule: write it once, write it twice, extract on the third repetition.
  4. useDebugValue adds a label to a custom hook in React DevTools. It has no effect on behavior and is development-only. Use it in hooks that would otherwise be hard to distinguish in large component trees.

Further Reading and Watching