skip to content

Custom Hooks

Extracting stateful logic into reusable functions that call other hooks — React's answer to mixins and HOC wrapper hell. Interviewers ask you to design one on the spot to see whether you separate logic from markup and can define a clean, non-leaky API.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

10

In React, when should logic inside a component be extracted into a custom hook, and when is a plain JavaScript function the better choice instead?

level: juniorimportance: must knowfreq 74%

answer

  1. organisation, not runtime change
  2. ask whether it needs React
  3. pure computation stays a function
  4. calls a hook, must be a hook
  5. repetition or a concept worth naming

basics

~20 s

Extract a custom hook when the logic calls other hooks, or when the same hook wiring repeats across components. If the logic is a pure computation over its arguments, keep it a plain function outside the component.

solid answer

~50 s

The dividing line is whether the logic needs React. If it calls `useState`, `useEffect`, `useContext` or any other hook, it has to live in a function named `use…` so it is called from a component or another hook — that is a custom hook. If it only transforms its arguments — formatting a date, summing line items, validating a string — it is a plain function and belongs in a module outside the component, where it is easier to test and reuse anywhere. Beyond that hard rule, I extract when the same stateful wiring appears in a second component, or when one component is visibly doing two jobs and the hook lets me name one of them. Extraction is about naming a concept and shrinking a component; it does not make anything run less often, so I do not extract three lines just to have fewer lines on screen.

code

javascript · 25 lines
javascript
import { useCallback, useState } from 'react';

// Plain function: touches no React API, so it stays a plain function.
export function totalCents(items) {
  return items.reduce((sum, item) => sum + item.priceCents * item.qty, 0);
}

// Custom hook: it calls useState, so it must be named use* and called from a component.
export function useSelection(initialIds = []) {
  const [selected, setSelected] = useState(() => new Set(initialIds));

  const toggle = useCallback((id) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  }, []);

  return { selected, toggle };
}

go deeper

for a junior

Be ready to state the rule plainly: if the logic calls a hook it must be a custom hook named use…, and if it just transforms its arguments it should stay a plain function outside the component.

for a middle

Explain that a custom hook is an ordinary function and the extraction is purely organisational — the state still belongs to the calling component — and name your extraction triggers: repetition, mixed concerns, a concept worth naming.

for a senior

Show judgement about cost: talk about when you deliberately do not extract, how option flags signal that two callers actually want two hooks, and how you keep pure logic in React-free modules so it stays testable and portable.

for a principal

Own the codebase-wide policy: where the boundary between React-free domain modules and hook wrappers sits, how that boundary keeps logic reusable on the server or in workers, and how you stop a shared hooks folder from becoming a dumping ground.

## What a custom hook actually is A custom hook is nothing more than a JavaScript function whose name starts with `use` and which calls other hooks. There is no registration step, no base class, no React API for declaring one. React never sees the hook as a unit: when your `useFilters()` calls `useState`, React records that state on the component that is currently rendering, exactly as if the `useState` call had been typed into the component body. The extraction moves source code, not runtime behaviour. That is why the naming convention matters. The `use` prefix is how both human readers and the `eslint-plugin-react-hooks` lint rules know that a call must obey the call-site rules — top level of a component or another hook, never inside a condition or loop. A function that calls hooks but is named `getFilters()` still behaves like a hook at runtime but is no longer checkable, and readers will move the call into an `if` without a second thought. ## The decision rule Ask one question: *does this logic need React?* - **It calls a hook** — state, refs, context, effects, subscriptions, transitions — then it must be a custom hook. There is no alternative; a plain function cannot call `useState`. - **It is a pure computation over its inputs** — then keep it a plain function, defined at module scope outside the component. It is trivially unit-testable without a renderer, reusable from a worker or a server file, and it cannot accidentally acquire a dependency on render order. ```javascript // Plain function: no React involved. export function totalCents(items) { return items.reduce((sum, item) => sum + item.priceCents * item.qty, 0); } // Custom hook: it calls useState, so it has to be one. export function useSelection(initialIds = []) { const [selected, setSelected] = useState(new Set(initialIds)); // … return { selected, setSelected }; } ``` A common mistake is turning `totalCents` into `useTotalCents` "for consistency". That is strictly worse: it can now only be called from a component, it drags a React dependency into a pure module, and the lint rules will police a call site that had no reason to be constrained. ## When extraction earns its keep Once the hard rule is satisfied, extraction is a judgement call driven by three signals: 1. **Repetition.** The same `useState` + `useEffect` + cleanup trio appears in a second component. Two copies of stateful wiring drift apart; one hook does not. 2. **Mixed concerns.** A component holds product logic *and* a scroll listener *and* a form draft. Pulling one concern into a named hook lets the component read as a list of concerns rather than a wall of wiring. 3. **A concept worth naming.** `useUnsavedChangesWarning()` communicates intent in a way that ten lines of effect never will. This is enough reason to extract even for a single caller — good naming is the point, not reuse count. ## When not to extract Extraction has a real cost: an extra file or function to open, an API surface to keep stable, and indirection between the state and the component that renders it. Skip it when the logic is three lines used once and has no name of its own; when the "hook" would need option flags to serve its two callers (a sign that they are actually two different behaviours); or when you are extracting hoping for a speed-up. It will not deliver one — the same `useState` and the same effect run for the same component either way. Performance work belongs to memoization and render-structure decisions, not to file organisation. ## What this buys you The payoff is testability and separation. A custom hook can be exercised on its own, its concern can evolve without touching the markup, and a component that reads `const { selected, toggle } = useSelection(ids)` states what it depends on in one line. That is the same argument as extracting any well-named function — React just adds the constraint that anything touching hooks must carry the `use` prefix and be called unconditionally.

  • Does extracting logic into a custom hook make a component render faster?
    No. The extracted `useState` and `useEffect` calls belong to the same component and run exactly as often as before — the hook is a source-code boundary, not a runtime one. The only indirect wins are that a smaller component is easier to restructure, and that a clearly-named hook makes it obvious which state actually drives which subtree.
  • Is it ever worth extracting a hook that has exactly one caller?
    Yes, when the extraction names a concept. `useUnsavedChangesWarning()` or `useAutoScrollToBottom()` compresses a page of wiring into one self-describing line, and lets that concern be changed or tested without touching the component's markup. Reuse is a nice bonus, not the entry price.
  • What happens if a function that calls hooks is not named with the use prefix?
    It still works at runtime, because React only cares that the hook calls happen unconditionally during a render. What you lose is checking: the React lint rules use the naming convention to identify hooks, so nothing warns when someone calls your `getFilters()` inside an `if` or a callback — and that is exactly where hook order breaks.

saying these in an interview costs you the question

  • Says every helper inside a React file should be named use*
  • Believes extracting a custom hook reduces how often code runs
  • Thinks a pure formatting function must become a hook to be reused
  • Extracts a hook for every few lines, creating indirection with no name
  • Claims custom hooks must be registered with React somehow

context

open as a page

Two React components each call the same custom hook, useCounter(). If one component increments its counter, does the other component's value change? Explain what a custom hook does and does not share.

level: middleimportance: must knowfreq 66%

basics

~20 s

No. A custom hook shares logic, not state: every call site gets its own independent state, so the two counters are unrelated. Sharing one value requires lifting it to a common parent, putting it in context, or using an external store.

open as a page

Write a React `useDebouncedValue(value, delay)` hook using useState and useEffect. Then explain why a `useDebouncedCallback(fn, delay)` variant needs a ref holding the latest `fn`.

level: middleimportance: must knowfreq 62%

basics

~20 s

useDebouncedValue keeps a state copy updated by an effect that starts a timeout and clears it in cleanup, so each new value restarts the delay. A callback version must keep the newest fn in a ref, because a debounced wrapper created once would otherwise fire a frozen closure.

open as a page

Write a React custom hook `useToggle(initial = false)` that returns a boolean and a function that flips it. Why should the flip call `setOn(v => !v)` instead of `setOn(!on)`?

level: juniorimportance: should knowfreq 52%

basics

~20 s

useToggle holds a boolean in useState and returns it with a toggler that calls setOn(v => !v). The updater form flips whatever the newest value is, so the toggle is still correct when the captured value is stale or two flips land in one event.

open as a page

When designing what a custom React hook returns, when should you return an array (a tuple, the way useState does) and when should you return an object?

level: middleimportance: should knowfreq 52%

basics

~20 s

Return a tuple for two or three positional values callers will frequently rename, as useState does. Return an object once there are several values or the set may grow, because names document meaning and adding a field breaks no existing caller.

open as a page

Implement a React `usePrevious(value)` hook that returns the value this component had on its previous render. Where must the ref be written, and what does the hook return the first time it runs?

level: middleimportance: should knowfreq 46%

basics

~20 s

Store the value in a useRef and overwrite it inside a useEffect that runs after every render, returning ref.current first. Reading before the effect writes yields the previous render's value; on the first render it is undefined.

open as a page

You are publishing a shared custom hook, useFilters(), that returns the current filter state plus functions to change it. Consumers memoize child components and list your functions in dependency arrays. What identity guarantees should the hook's return value make, and how do you implement them?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Promise that the functions a hook returns keep the same identity for the life of the component, and that data changes identity only when it really changed. Implement stable functions with useCallback plus updater-form updates or a latest-value ref, so no state sits in their dependency list.

open as a page

Implement a React `useLocalStorage(key, initialValue)` hook that returns a value and a setter which also writes to localStorage. What does a naive implementation get wrong about reading storage, parsing, and server rendering?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Read storage in a lazy useState initializer, not on every render; wrap JSON.parse and every storage call in try/catch because parsing and quota both throw; support functional updates so the write matches the new state; and on the server, where localStorage does not exist, render the initial value and read storage after mount.

open as a page

Write a React `useOnClickOutside(ref, handler)` hook that closes a dropdown when the user clicks elsewhere. Which document event should it listen for, and how do you stop a new inline `handler` from tearing down and re-attaching the listener on every render?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Attach a document listener for pointerdown, ignore the event when ref.current contains event.target, and remove the listener in effect cleanup. Keep the handler in a ref refreshed each commit so the subscribing effect depends only on the ref, not on a handler identity that changes every render.

open as a page

A shared useDashboard() hook in your codebase takes an options object with eight boolean flags and returns fifteen fields, and every team is now afraid to change it. How do you decide where to split it, and what do you weigh when drawing hook boundaries in a shared library?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Split by concern and lifetime, not by size: each hook should own one source of truth or one subscription. Behaviour-selecting flags are the strongest signal that several hooks were fused, so extract those first and keep a thin composite for existing callers.

open as a page