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
useand 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
useSomethingfunction that they all call. - use prefix
- The naming rule: hook names start with
useand 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.
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.
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.
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.
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
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
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 to0: how many lines are printed now, and why? - 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
Both `Likes` call the same hook, but the second one is still at 0. A custom hook shares code, not state.
Preview
Console
Nothing logged yet.Expected console output
Likes: 2 | Likes: 0 | Points: 110 | ResetThe 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
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: InboxClick 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
Console
Nothing logged yet.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.
Break #2
Name a hook without the use prefix
Rename useCounter to getCounter (in its definition and its calls) and run the linter.
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 (
useChatRoomcan calluseOnlineStatusanduseEffectEvent). 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
A custom hook is an ordinary JavaScript function that (1) has a name starting with
usefollowed by a capital letter, such asuseCounteroruseOnlineStatus, 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 callsuseCounter(), React only sees theuseStatecall inside it, as if you had written that line in the component. - 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
useSomethingfunction,returnwhatever 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
Each call gets its own state. A custom hook shares logic, never state. If
LikesandPointsboth calluseCounter(), each call runs its ownuseState, 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
**Why the
useprefix 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. Theuseprefix is how people and tools know a function follows those rules and may call hooks. Theeslint-plugin-react-hookslinter checks calls inside any function nameduseXand reports an error if a function with another name (such asgetCounter) calls a hook. A function that calls no hooks should not start withuse: make it a plain function (Topic 10.6). - 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), wherepageTitleis worked out from theunreadstate. 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
Returning values and functions. A hook can return anything: one value (
const isOnline = useOnlineStatus()), an array likeuseStatedoes (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
What is a custom hook, and what makes a function one?
If two components call the same custom hook, do they share state? Explain why.
Why must custom hook names start with use?
How do you pass changing data into a custom hook, and how does the hook react when it changes?
Practice
Write a hook useCharacterCount(text, max) that returns { count, left, tooLong } and use it under a textarea.
Extract a useHover() hook that returns [hovered, { onMouseEnter, onMouseLeave }] and use it on two cards that each show Hovered! independently.
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
useSomethingand 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.