# Custom Hooks

Course: [React](https://www.devtutorials.ai/learn/react) · Section: React Hooks
Source: https://www.devtutorials.ai/learn/react/hooks/custom-hooks

> What custom hooks are and why to use them, reusing logic across components, creating and naming hooks, using built-in hooks inside them, and useToggle and useForm examples.

## What Are Custom Hooks?

A **custom hook** is a function that you write yourself, whose name starts with `use`, and that uses other hooks inside it. It lets you take some hook logic out of a component and use it again in other components.

For example, `useToggle` could hold the logic for "show or hide". Any component can then call `useToggle()` instead of writing that logic again.

### Real-World Example

In a bookshop app, the book list, the order list and the author page all load data from the server. Each one needs `data`, `loading` and `error` state and a `useEffect`.

The team moves this code into one custom hook, `useFetch(url)`. Each page now calls `const { data, loading, error } = useFetch("/books")` in one line.

A custom hook is a normal function that starts with `use` and calls other hooks. It shares **logic**, not data. Each component that uses it gets its own copy of the state.

## Why Custom Hooks?

Without custom hooks, you often copy the same lines of state and effect code into many components.

- **Less repeated code** — Write the logic once and use it everywhere.
- **Cleaner components** — The component shows what it does, and the hook hides how it is done.
- **One place to fix bugs** — Fix the hook once, and every component that uses it is fixed.
- **Easier to test** — A small hook is easier to check than a big component.

## Reusing Logic Across Components

Look at the two components below. Both have the same counter logic: a `useState`, an "add" function and a "reset" function.

    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

The three logic lines are the same in both components. If you wanted to change how reset works, you would have to change it twice. A custom hook fixes this.

## Creating Custom Hooks

To create a custom hook:

1. Write a function whose name starts with `use`, such as `useCounter`.
2. Move the hook logic into it.
3. Return what the component needs, such as the value and the functions.

    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Click the **+** buttons. Each counter changes on its own. Both components use `useCounter`, but each one gets **its own** `count`. The hook shares the logic, not the number.

Look at the last line of the hook:

```tsx
return { count, add, reset };
```

The hook gives back an object with three things. The component picks them up as `likes.count`, `likes.add` and `likes.reset`.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-c-reuse" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-c-reuse">One custom hook, useCounter, is used by two components. Each component gets its own count</title>
  <rect x="170" y="10" width="180" height="48" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="260.0" y="31.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>useCounter()</text>
  <text x="260.0" y="48.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>the logic, written once</text>
  <path d="M220 58 L110 104" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M110 104 L116.3 97.6 L119.0 104.0 Z" style={{ fill: "var(--faint)" }} />
  <path d="M300 58 L410 104" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M410 104 L401.0 104.0 L403.7 97.6 Z" style={{ fill: "var(--faint)" }} />
  <rect x="20" y="106" width="180" height="52" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="110.0" y="129.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>LikeButton</text>
  <text x="110.0" y="146.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>its own count: 3</text>
  <rect x="320" y="106" width="180" height="52" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="410.0" y="129.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>CartCounter</text>
  <text x="410.0" y="146.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>its own count: 5</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="2.4s" repeatCount="indefinite" path="M220 58 L110 104 M300 58 L410 104" />
  </circle>
</svg>
<figcaption>The hook shares the logic. Each component that calls it gets its own separate state.</figcaption>
</figure>

If two components must show the **same** number, a custom hook alone is not enough. Keep the state in their parent and pass it down, or use context.

### Returning an Array or an Object

A custom hook can return any value. Two styles are common:

- **An object** — `return { count, add, reset };` The component reads them by name. Good when there are many items.
- **An array** — `return [isOn, toggle] as const;` The component can pick its own names, like `useState`. Good for two items.

`as const` tells TypeScript that the array always has exactly these two items, in this order.

## Naming Conventions

- **Always start with `use`** — `useCounter`, `useToggle`, `useForm`. React and its tools use the `use` start to check the rules of hooks.
- **Say what it does** — `useWindowWidth` is clear. `useData` is not.
- **One hook, one job** — If a hook does many things, split it into smaller hooks.
- **One hook per file** — Name the file after the hook: `useCounter.ts`. Use `.ts` when the hook returns no TSX.

If your function does not call any other hook, it is a normal helper function. Do not start its name with `use`.

## Using Built-in Hooks Inside Custom Hooks

A custom hook can call any built-in hook: `useState`, `useEffect`, `useRef`, and so on. It can also call other custom hooks.

This hook uses `useState` and `useEffect` to keep track of the width of the window.

<figure className="react-fig">
<svg viewBox="0 0 520 150" role="img" aria-labelledby="fig-c-window" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-c-window">useWindowWidth hides useState and useEffect inside. App only gets a number</title>
  <rect x="10" y="10" width="300" height="130" rx="10" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeDasharray: "5 4" }} />
  <text x="160" y="30" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>useWindowWidth()</text>
  <rect x="25" y="45" width="130" height="40" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="90.0" y="62.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>useState</text>
  <text x="90.0" y="79.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>holds the width</text>
  <rect x="165" y="45" width="130" height="40" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="230.0" y="62.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>useEffect</text>
  <text x="230.0" y="79.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>listens for resize</text>
  <text x="160" y="118" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 400, fontFamily: "var(--sans)" }}>inside the hook</text>
  <path d="M310 75 L370 75" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M370 75 L361.7 78.5 L361.7 71.5 Z" style={{ fill: "var(--faint)" }} />
  <text x="340.0" y="67.0" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>width</text>
  <rect x="375" y="52" width="135" height="46" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="442.5" y="72.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>App</text>
  <text x="442.5" y="89.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>gets a number</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="1.6s" repeatCount="indefinite" path="M310 75 L370 75" />
  </circle>
</svg>
<figcaption>A custom hook can use built-in hooks inside. The component only sees the result.</figcaption>
</figure>

      <p>The preview is {width}px wide.</p>
      <p>{width < 400 ? "Small screen: show one book per row." : "Big screen: show three books per row."}</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Drag the handle between the code and the preview to make the preview wider or narrower. The width updates as you drag.

`App` does not know about events or effects. It just calls `useWindowWidth()` and gets a number. All the hard work is inside the hook.

The `return () => ...` line inside the effect is a **cleanup function**. It stops listening when the component is removed. You will learn about it in the next lesson.

## Toggle Hook Example

A **toggle** is a true/false value that flips. It is used for show/hide buttons, menus, dark mode, and more.

      <h3>Atomic Habits</h3>

      <button onClick={toggleDetails}>{showDetails ? "Hide" : "Show"} details</button>
      {showDetails && <p>by James Clear · 320 pages · ₹499</p>}

      <br />
      <button onClick={toggleReviews}>{showReviews ? "Hide" : "Show"} reviews</button>
      {showReviews && <p>"Tiny changes, big results." — Asha</p>}
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

`useToggle` is called twice, so there are two separate true/false values. Details start hidden. Reviews start shown, because of `useToggle(true)`.

Look at this line:

```tsx
const [showDetails, toggleDetails] = useToggle();
```

Because the hook returns an array, you choose the names, just like with `useState`.

## Form Handling Hook Example

Forms repeat the same code again and again: one state object, one change handler, and a reset. A `useForm` hook can hold all of it.

<figure className="react-fig">
<svg viewBox="0 0 520 150" role="img" aria-labelledby="fig-c-form" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-c-form">useForm: typing calls handleChange, which updates values; reset puts the start values back</title>
  <rect x="10" y="50" width="120" height="46" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="70.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Input boxes</text>
  <text x="70.0" y="87.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>name="title"</text>
  <path d="M130 73 L180 73" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M180 73 L171.7 76.5 L171.7 69.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="170" y="30" width="180" height="86" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="260.0" y="70.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>useForm</text>
  <text x="260.0" y="87.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>values, handleChange, reset</text>
  <path d="M335 60 L385 40" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M385 40 L378.6 46.3 L376.0 39.8 Z" style={{ fill: "var(--faint)" }} />
  <text x="360.0" y="42.0" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>submit</text>
  <rect x="390" y="15" width="120" height="44" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="450.0" y="42.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Save the book</text>
  <path d="M335 90 L385 110" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M385 110 L376.0 110.2 L378.6 103.7 Z" style={{ fill: "var(--faint)" }} />
  <text x="360.0" y="118" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>reset()</text>
  <rect x="390" y="90" width="120" height="44" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="450.0" y="109.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Empty boxes</text>
  <text x="450.0" y="126.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>start values</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="3s" repeatCount="indefinite" path="M130 73 L180 73 M335 60 L385 40 M335 90 L385 110" />
  </circle>
</svg>
<figcaption>The hook keeps the form values and gives the component everything it needs.</figcaption>
</figure>

      <input name="title" placeholder="Title" value={values.title} onChange={handleChange} />
      <input name="author" placeholder="Author" value={values.author} onChange={handleChange} />
      <button type="submit">Add book</button>
      <ul>
        {books.map((b, i) => <li key={i}>{b}</li>)}
      </ul>
    </form>
  );
}
`,
    "App.css": `.form { display: grid; gap: 6px; max-width: 280px; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

The component is short. It only says what the form has (`title` and `author`) and what to do on submit. The hook handles typing and resetting.

Look at the first line of the hook:

```tsx
export function useForm<T extends Record<string, string>>(startValues: T) {
```

`<T ...>` makes the hook work with any form. `T` stands for "the shape of your form values". Here, `T` becomes `{ title: string; author: string }`. So TypeScript knows that `values.title` exists.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| Naming the hook without `use`, like `counter()` | Hook rules are not checked, and bugs are hard to find | Start the name with `use` |
| Expecting two components to share the same state | Changing one does not change the other | Each call gets its own state. Use props or context to share data |
| Calling the custom hook inside an `if` | React error about hooks | Call it at the top of the component |
| Forgetting to return the values from the hook | The component gets `undefined` | Add `return { ... }` or `return [ ... ]` |
| Returning an array without `as const` | TypeScript mixes up the types of the items | Write `return [value, fn] as const;` |
| One hook doing many unrelated jobs | Hard to read and reuse | Split it into small hooks with one job each |

</div>

## Check Yourself

## Exercises

## What to Remember

- A custom hook is a function that starts with `use` and calls other hooks. It shares logic between components.
- Each component that calls the hook gets its own state.
- Return what the component needs: an object for many items, or an array with `as const` for two.

Next: Component Lifecycle
