skip to content

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