Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate React 16.8+ ~31 min· topic 1 of 6

Topic 10.1

Custom Hooks: Reusing Logic

In one line

A custom hook is a function whose name starts with use and that calls other hooks; it lets many components share stateful logic, while every component that calls it still gets its own separate state.

Think of it like this

A cake recipe card. You and your friend both follow the same card, but you each bake your own cake in your own kitchen. If you burn yours, your friend's cake is fine. A custom hook is the recipe card: the steps are shared, but each component that follows it gets its own ingredients (its own state).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Custom hook
Your own function whose name starts with use and that calls other hooks, so several components can reuse the same logic.
Extracting a hook
Moving repeated state and effect code out of components into one useSomething function that they all call.
use prefix
The naming rule: hook names start with use and a capital letter (useCounter). It tells people and the linter the function may call hooks.
Reactive value
A value that can change between renders: props, state and anything computed from them.
Logic vs state
A custom hook shares the code (logic). Each component that calls it gets its own copy of the data (state).
eslint-plugin-react-hooks
The official linter plugin that checks the rules of hooks and effect dependencies, including inside your custom hooks.
Refactor
Changing how code is organised without changing what it does.

Step by step

01Spot the repeated logic

Two components each keep a number in state and have the same "add a step" and "reset" code. The only differences are the starting number and the step. Repeated state-plus-handlers code is the signal for a custom hook.

Before.jsxwhole filejsx
function Likes() {
  const [count, setCount] = useState(0);
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>Likes: {count}</button>;
}

function Points() {
  const [count, setCount] = useState(100);
  const increment = () => setCount((c) => c + 10);
  const reset = () => setCount(100);
  // ...
}

02Cut the logic into a use function

Create useCounter(initial, step). Move the useState call and the handlers inside it. The differences (start and step) become parameters. Return what components need.

useCounter.jswhole filejsx
import { useState } from "react";

export function useCounter(initial = 0, step = 1) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount((c) => c + step);
  const reset = () => setCount(initial);
  return { count, increment, reset };
}

03Call it from each component

Each component now has one line of logic. When Likes renders, it calls useCounter, which calls useState. From React's point of view, Likes called useState: the hook's hooks become the component's hooks, in the same order every render.

Call it from each componentdiagram
Rendering diagram…

04Each call has its own state

The live example below puts two <Likes /> and one <Points /> on the page and clicks them from code. The two Likes buttons don't affect each other: every component instance gets its own useState slot.

05Pass reactive values in

A hook's parameters can be props or state. Because the component calls the hook on every render, the hook always sees the latest value. Put it in an effect's dependency list and the effect re-runs when it changes.

useDocumentTitle.jswhole filejsx
export function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]); // re-sync when the title changes
}

// in a component:
useDocumentTitle(unread > 0 ? `(${unread}) Inbox` : "Inbox");

06Put hooks in their own files

In a project, hooks usually live next to the components that use them, or in a hooks/ folder when many features share them: src/hooks/useCounter.js. Export them with a named export so the name stays the same everywhere (Topic 2.2).

Try it yourself

  1. 1

    Add a third counter

    In the first example, add function Stars() { const { count, increment } = useCounter(5, 5); ... } and render it. Predict: does it start at 5 and go up by 5? Does it change the Likes buttons? Check the console line after adding it to the click script.

  2. 2

    Change the title format

    In the inbox example, change the title to Inbox: ${unread} new (with backticks). Predict the four log lines before you run it. Then change the starting value to 0: how many lines are printed now, and why?

  3. 3

    Give useField a new rule

    Add a third field: const age = useField("", (v) => (Number(v) >= 13 ? null : "You must be 13 or older"));. You wrote zero new state or handler code for it: that is the payoff of the hook.

Code & diagrams

One hook, separate state for every caller New tab

Both `Likes` call the same hook, but the second one is still at 0. A custom hook shares code, not state.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Likes: 2 | Likes: 0 | Points: 110 | Reset
Passing a reactive value into a hook New tab

The first line comes from the first render. Three clicks bring `unread` to 0. The fourth click sets 0 again, the title string is the same, so the effect doesn't re-run: no fifth line.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

title set to: (3) Inbox
title set to: (2) Inbox
title set to: (1) Inbox
title set to: Inbox
Returning values and functions: a form field hook New tab

Click into a field and leave it empty to see its error. Each `useField` call has its own value and touched flag, and the component only uses what the hook returns.

Preview

Press Run to see the page.

Console

Nothing logged yet.
Where a custom hook's state livesdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Expect two components to share a hook's state

Write function Total() { const { count } = useCounter(0, 1); return <p>Total likes: {count}</p>; }, render it under the two Likes buttons, and click Likes a few times.

terminal
$ Preview
── what you'll see ──
Likes: 3
Likes: 0
Total likes: 0

Break #2

Name a hook without the use prefix

Rename useCounter to getCounter (in its definition and its calls) and run the linter.

terminal
$ npx eslint src/
── what you'll see ──
4:29 error React Hook "useState" is called in function "getCounter" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks

Myth vs fact

Myth

Components that call the same custom hook share its state.

Fact

Every call runs its own hooks, so every component instance gets its own state. Hooks share logic only.

Myth

Custom hooks are a special React feature you must register.

Fact

They are plain functions. React only sees the built-in hooks they call. The use name is a convention that the linter and the React Compiler rely on.

Myth

A hook must return an array like useState.

Fact

A hook can return nothing, one value, an array or an object. Arrays suit two items the caller will rename; objects suit three or more named items.

Myth

Moving code into a hook makes it run less often.

Fact

A hook's body runs on every render of the component that calls it, exactly like the code it replaced. Its effects follow their dependency lists as usual.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Because a hook's state lives in the calling component's fiber (Topic 0.2), React DevTools shows custom hooks nested under the component in the hooks panel, labelled by name (Counter, DocumentTitle), with the built-in hooks inside. useDebugValue (Topic 10.4) adds a label there.

  • ▸

    The React Compiler (Topic 16.5) treats use-prefixed functions as hooks: it assumes they follow the rules of hooks and memoises around them. A function that calls hooks under another name can confuse both the linter and the compiler.

  • ▸

    Custom hooks are composable: a hook can call other custom hooks (useChatRoom can call useOnlineStatus and useEffectEvent). This is the real advantage over older reuse patterns like mixins, HOCs and render props (Topic 10.5): composition is just function calls, with explicit inputs and outputs and no name clashes.

  • ▸

    Hooks can't be called conditionally, but you can pass a condition into a hook: useInterval(tick, running ? 1000 : null). The hook always runs; it decides inside what to do. This keeps the call order stable (Topic 10.2).

Remember this

  1. 1

    A custom hook is an ordinary JavaScript function that (1) has a name starting with use followed by a capital letter, such as useCounter or useOnlineStatus, and (2) calls other hooks inside it: useState, useEffect, useRef, useContext, or other custom hooks. That's all. There is no special API to register it. React doesn't even know your hook exists: when a component calls useCounter(), React only sees the useState call inside it, as if you had written that line in the component.

  2. 2

    Extracting a hook is a refactor (changing how code is organised without changing what it does). You spot state and effects that two components repeat, cut them out of one component into a new useSomething function, return whatever the component needs, and replace the code in both components with one call. The components get shorter and read like a description: const isOnline = useOnlineStatus(); says what the component needs, not how it is done.

  3. 3

    Each call gets its own state. A custom hook shares logic, never state. If Likes and Points both call useCounter(), each call runs its own useState, so each component gets its own counter. Two <Likes /> on the page also count separately. If you need components to share the same value, lift the state up (Topic 6.3), put it in context (Topic 9.2) or use an outside store (Topic 10.3).

  4. 4

    **Why the use prefix matters.** Hooks have rules (Topic 4.3): call them only at the top level of a component or of another hook, never inside conditions or loops, because React matches hooks by their call order. The use prefix is how people and tools know a function follows those rules and may call hooks. The eslint-plugin-react-hooks linter checks calls inside any function named useX and reports an error if a function with another name (such as getCounter) calls a hook. A function that calls no hooks should not start with use: make it a plain function (Topic 10.6).

  5. 5

    Passing reactive values in. A reactive value is a value that can change between renders: props, state, and anything calculated from them. You can pass them into a hook as arguments: useDocumentTitle(pageTitle), where pageTitle is worked out from the unread state. Every time the component renders, it calls the hook again with the latest value. If the hook uses that value in an effect and lists it as a dependency, the effect re-syncs whenever the value changes (Topic 7.5). The hook's code re-runs on every render of its component, exactly like code written in the component body.

  6. 6

    Returning values and functions. A hook can return anything: one value (const isOnline = useOnlineStatus()), an array like useState does (const [on, toggle] = useToggle(), handy when callers rename the parts), or an object (const { count, increment, reset } = useCounter(), clearer when there are three or more things). Return functions for the actions the component may take, so the component never touches the hook's internal setters directly. Topic 10.6 shows how to keep those returned functions stable.

Explain it without notes

01

What is a custom hook, and what makes a function one?

02

If two components call the same custom hook, do they share state? Explain why.

03

Why must custom hook names start with use?

04

How do you pass changing data into a custom hook, and how does the hook react when it changes?

Practice

01

Write a hook useCharacterCount(text, max) that returns { count, left, tooLong } and use it under a textarea.

02

Extract a useHover() hook that returns [hovered, { onMouseEnter, onMouseLeave }] and use it on two cards that each show Hovered! independently.

03

This component repeats logic twice. Extract a hook and rewrite it: two checkboxes, each with const [x, setX] = useState(false) and an onChange={() => setX(!x)}.

Trade-offs

  • ↔

    A hook hides details, which makes components easier to read, but also hides how much work happens (effects, subscriptions). Name hooks clearly and keep them small so readers can guess what they do.

  • ↔

    Extracting too early creates hooks with many options to cover cases that don't exist yet. Wait until the logic repeats (or is complex enough to name), then extract.

Done when you can

  • I can extract repeated state and effect code into a custom hook.

  • I can explain why every component calling a hook gets its own state.

  • I name every hook useSomething and know what the linter checks.

  • I can pass props and state into a hook and use them as effect dependencies.

  • I can choose between returning a value, an array or an object.