Overview

Hooks are the only state and side-effect surface in a function component. The rules around them are small, mechanical, and absolute. This page covers the four decisions a hooks-heavy file forces: which state primitive, whether to write an effect at all, how to name a custom hook, and when manual memoization is worth the bookkeeping. The umbrella rule set lives in react.

Call hooks at the top level, every render

Hooks run only at the top level of a function component or another hook. Never inside a condition, a loop, an early return, an event handler, or a class. React tracks hook identity by call order; a conditional hook desyncs the order and the component breaks.

// Wrong: hook after a conditional return.
if (!user) return null;
const [count, setCount] = useState(0);
 
// Right: hook first, branch on its value.
const [count, setCount] = useState(0);
if (!user) return null;

Run eslint-plugin-react-hooks in CI. The rules-of-hooks rule is not optional, and exhaustive-deps catches stale-closure bugs before they ship.

Pick useState for atomic values, useReducer for shape

Reach for useState when the value is a single primitive or an independent piece of state. Reach for useReducer when several pieces move together, when the next state depends on the previous in non-trivial ways, or when a test wants to assert against a pure transition function.

// useState: independent atom.
const [open, setOpen] = useState(false);
 
// useReducer: coordinated transitions.
const [state, dispatch] = useReducer(reducer, { status: "idle", items: [], error: null });

If three useState calls always update in lockstep, they are one reducer in disguise. Collapse them.

Treat useEffect as the escape hatch

Most components do not need an effect. Compute derived values during render. Read URL state from the router. Mutate through event handlers and server actions. Reach for useEffect only when synchronizing with something outside React: a DOM API, a subscription, a localStorage write, an analytics call.

// Wrong: effect mirrors a prop into state.
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
 
// Right: derive in render.
const full = `${first} ${last}`;

When an effect is legitimate, name what it synchronizes (“syncs the document title to the current route”) and keep the dependency array exhaustive. Cleanup is mandatory for subscriptions, timers, and listeners; the return function runs on unmount and before the next effect.

Name custom hooks useThing, return one shape

A custom hook is any function that calls another hook. Name it with the use prefix so the linter can enforce the hook rules at the call site. Return a single shape: an array for two-or-three-value tuples, an object once the count grows past that.

function useDebouncedValue<T>(value: T, ms = 200): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(id);
  }, [value, ms]);
  return debounced;
}

A hook is the right unit when two components share state or effect logic. A utility function is the right unit when the logic is pure. Do not wrap a pure function in useMemo and call it a hook.

Skip useMemo and useCallback until the profiler asks

The React 19 compiler memoizes most of what teams used to write by hand. Bare useMemo on a cheap expression adds bookkeeping and slows the render. Reach for manual memoization only when the React DevTools Profiler shows the cost, and the wrapped value is one of these:

  • A stable reference passed to a memo() child whose render is expensive.
  • A computation that is genuinely heavy: parsing, large array transforms, regex compilation.
  • A dependency of another hook whose identity must stay stable across renders.

useCallback follows the same rule. A handler passed to a native DOM element does not need it.

Read context, do not lift through it

useContext is a read primitive, not a state container. Pair it with useReducer or a focused store when the value changes. A context that wraps the whole app and stores high-frequency state (mouse position, scroll offset, form values) re-renders every consumer on every change. Use react-state-management guidance: low-frequency globals in context, everything else in a store with selector hooks.