Writing Custom Hooks

Extracting stateful logic into reusable functions, and the rules of hooks.

What you'll learn

  • What makes a function a hook
  • Extracting repeated stateful logic without a wrapper component
  • The two rules of hooks, and the reason behind them
  • What is shared between components using the same hook — and what is not

A hook is just a function

A custom hook is a function whose name starts with use and which calls other hooks. There is no API to register, no special import. The naming convention is what tells React — and the linter — to apply the rules of hooks to it.

Here is a component with a small amount of logic worth naming:

import { useToggle } from "./useToggle.js";

export default function App() {
  const [showDetails, toggleDetails] = useToggle();
  const [isSubscribed, toggleSubscribed] = useToggle(true);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, fontSize: 13 }}>
      <button onClick={toggleDetails}>
        {showDetails ? "Hide" : "Show"} details
      </button>
      {showDetails && <p>Here are the details.</p>}

      <p>
        <label>
          <input type="checkbox" checked={isSubscribed} onChange={toggleSubscribed} />
          Subscribed
        </label>
      </p>
    </div>
  );
}

Two independent pieces of state, one shared piece of logic.

State is not shared

This is the part people expect to work the other way round. A custom hook shares logic, not state. Every component that calls the hook gets its own state, exactly as if it had called useState directly.

The two counters below use the same hook and move independently:

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

function Counter({ label }) {
  const { count, increment, reset } = useCounter();

  return (
    <p>
      {label}: {count}{" "}
      <button onClick={increment}>+1</button>{" "}
      <button onClick={reset}>reset</button>
    </p>
  );
}

export default function App() {
  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, fontSize: 13 }}>
      <Counter label="First" />
      <Counter label="Second" />
    </div>
  );
}

To genuinely share one value between components, you need state in a common parent or useContext.

A hook with an effect

Hooks come into their own when they bundle an effect and its cleanup, so callers cannot forget the cleanup half. Resize the preview pane:

import { useWindowWidth } from "./useWindowWidth.js";

export default function App() {
  const width = useWindowWidth();

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p>The preview is {width}px wide.</p>
      <p>{width < 500 ? "Narrow layout" : "Wide layout"}</p>
    </div>
  );
}

The component reads like a description of what it needs — a width — with the listener, the state, and the teardown all named and hidden.

The rules of hooks

There are two, and they follow from one implementation detail: React tracks hooks by call order, not by name. The first useState in a component is matched with the first useState from the previous render, and so on.

1. Only call hooks at the top level. Not inside conditions, loops, or nested functions:

// Broken: on renders where isLoggedIn is false, the hook order changes,
// and React hands the wrong state to the wrong call.
if (isLoggedIn) {
  const [name, setName] = useState("");
}

// Fine: the condition goes inside.
const [name, setName] = useState("");
if (isLoggedIn) {
  // ...
}

2. Only call hooks from React functions. From components, or from other hooks — not from ordinary functions, class methods, or event handlers.

The use prefix is what lets tooling check this for you. Name a hook getToggle and the linter will not know it contains hooks.

What belongs in a hook

A good custom hook has a job you can name: useToggle, useWindowWidth, useFetch, useLocalStorage. If you cannot name it without "and", it is probably two hooks.

Do not extract just to shorten a component. Extract when logic is duplicated, when a component is doing several unrelated things at once, or when a subscribe and unsubscribe pair should always travel together.

Common mistakes

  • Not starting the name with use. The rules stop being enforced.
  • Expecting shared state. Each caller gets its own.
  • Calling a hook conditionally. Move the condition inside the hook.
  • Extracting too early. One use with no duplication rarely earns a hook.

Exercise

App has the same show/hide logic written twice. Extract it into a useToggle hook in /useToggle.js and use it for both panels.

import { useState } from "react";

export default function App() {
  const [showFirst, setShowFirst] = useState(false);
  const [showSecond, setShowSecond] = useState(false);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, fontSize: 13 }}>
      <button onClick={() => setShowFirst(!showFirst)}>Toggle first</button>
      {showFirst && <p>First panel</p>}

      <button onClick={() => setShowSecond(!showSecond)}>Toggle second</button>
      {showSecond && <p>Second panel</p>}
    </div>
  );
}