skip to content

A React component holds a callback in state with `const [handler, setHandler] = useState(() => defaultHandler)`. A later call to `setHandler(nextHandler)`, where `nextHandler` is also a function, leaves `handler` holding something unexpected. Why, and what should be written instead?

level: middleimportance: nice to knowfreq 26%

answer

  1. a function argument is never a value
  2. React runs what it is handed
  3. two overloads, both bite here
  4. wrap it in one more arrow
  5. a ref may fit better than state

basics

~20 s

A function argument means updater, not value. React calls nextHandler with the current state and stores whatever it returns, so the state becomes that return value. Store a function by wrapping it: setHandler(() => nextHandler).

solid answer

~50 s

Both `useState` and its setter treat a function argument specially: on `useState` it means "lazy initializer", and on the setter it means "updater". So `setHandler(nextHandler)` does not store `nextHandler` — React calls it with the current state and stores the return value, which is usually `undefined` or garbage, and the next click blows up with "handler is not a function". React has no way to tell a value that happens to be a function from a function you meant it to call, so the overload always wins. The fix is one extra arrow: `setHandler(() => nextHandler)`, an updater that ignores the previous state and returns the function you want stored. The initial value needs the same treatment, which is why the working code above already reads `useState(() => defaultHandler)`. Often a ref or a plain object wrapper is a better home for a callback you never render.

code

jsx · 16 lines
jsx
import { useState } from 'react';

const greet = () => 'hello';
const shout = () => 'HELLO';

export default function Demo() {
  // arrow at both ends: initializer and updater
  const [speak, setSpeak] = useState(() => greet);

  return (
    <>
      <output>{speak()}</output>
      <button onClick={() => setSpeak(() => shout)}>use shout</button>
    </>
  );
}

go deeper

for a junior

Recall the rule literally: to keep a function in state you write an extra arrow, at initialisation and at every set. Recognising the wrapper in code you read is enough at this level.

for a middle

Explain the overload — a function argument is an initializer or an updater — and predict exactly what ends up in state when the wrapper is missing, including the delayed 'not a function' error.

for a senior

Diagnose it from the symptom rather than the source line, and push the design question: whether the callback should be in state at all, or in a ref, or derived from a plain value.

for a principal

Treat it as an API-ergonomics lesson worth generalising — an overload keyed on a runtime type check is convenient until values of that type are legitimate data, and your team's conventions should keep functions out of serialisable state.

## One argument, two meanings The state setter is overloaded. Given `setX(arg)`: - if `arg` is **not** a function, React stores it as the next state; - if `arg` **is** a function, React treats it as an updater, calls it with the current state, and stores the result. `useState(arg)` has the mirror-image overload: a non-function is the initial value, a function is a lazy initializer that React calls to produce the initial value. There is no third channel. React inspects the argument at runtime with a `typeof` check; it cannot know whether you intended "here is a function to call" or "here is a function to keep". ## What actually happens in the broken code ```jsx const [handler, setHandler] = useState(() => defaultHandler); // intent: store nextHandler setHandler(nextHandler); // React calls nextHandler(defaultHandler) ``` React invokes `nextHandler` with the current state as its argument and stores whatever comes back. Two failure shapes follow. If `nextHandler` returns nothing, state becomes `undefined` and the next `handler(...)` call throws `handler is not a function`. If `nextHandler` has a side effect, it just ran at an unexpected moment, with a function as its argument — a genuinely confusing bug to read. ## The fix Wrap it in an updater that ignores the previous state: ```jsx setHandler(() => nextHandler); ``` The outer arrow is the updater; React calls it, ignores the argument, and stores `nextHandler` itself. The same reasoning explains the initialisation: `useState(defaultHandler)` would call `defaultHandler` at mount, so you write `useState(() => defaultHandler)`. A reviewer's tell: any `useState` holding a function should have arrows at both ends, and if only one side has it, one of the two paths is broken. ## Alternatives that dodge the overload **Wrap it in an object.** `useState({ fn: defaultHandler })` and `setHandler({ fn: nextHandler })` — the argument is never a function, so the overload never fires, and the intent reads clearly. The cost is `handler.fn(...)` at every call site. **Do not put it in state at all.** State exists to drive rendering. If swapping the callback does not change what is on screen, a ref is the better home — a mutable box you assign to, with no overload and no re-render. Keep the function in state only when the UI genuinely differs depending on which one is current. **Derive it instead.** Very often the "current callback" is a function of some other state — a mode, a strategy name, a selected tool. Store that plain value and pick the function during render from a lookup table. This removes the problem entirely and makes the state serialisable and debuggable, which functions in state never are. ## Why interviewers ask this It is a small question with a precise answer, and it checks whether you understand that React's ergonomic overloads have a cost: two of the hook's four argument positions reinterpret functions. A candidate who has internalised "function argument means call me" answers instantly and usually volunteers that state is a poor place for a callback in the first place — which is the answer behind the answer.

  • What is the state value immediately after setHandler(nextHandler), if nextHandler takes no arguments and returns nothing?
    `undefined`. React called nextHandler with the current state, ignored the fact that it took no parameters, and stored the return value. The failure usually surfaces one interaction later as a "not a function" TypeError, which is why the bug is hard to trace to its cause.
  • Would storing the callback in an object avoid the problem?
    Yes — `setHandler({ fn: nextHandler })` passes an object, so the updater overload never fires and the function is stored untouched. You pay for it with `handler.fn(...)` at every call site, and you still owe yourself the question of whether the callback belongs in state at all.
  • When does a callback genuinely belong in state rather than a ref?
    When swapping it must produce a re-render — for example the UI displays which strategy is active, or child props derive from it. If nothing rendered depends on which function is current, a ref holds it with no overload, no wrapper arrow and no extra render.

saying these in an interview costs you the question

  • Says React stores any argument as-is, functions included
  • Blames the bug on the arrow function's `this` binding
  • Thinks only useState has the function overload, not the setter
  • Adds the wrapper arrow to the setter but not to the initializer
  • Claims wrapping in useCallback would fix the storage problem

context