skip to content

In React, a function component's body runs again from scratch on every render, yet the value returned by useState is still there. Where does React actually keep that state, and what follows from that when the same component is rendered in two places?

level: middleimportance: should knowfreq 45%

answer

  1. not the function, not the closure
  2. per-position storage, per-instance identity
  3. matched by order, not by name
  4. a linked list of records
  5. it dies when the position unmounts

basics

~20 s

React stores hook state on the fiber node representing that component instance, as a chain of hook records matched by call order. Every rendered position has its own fiber, so two instances of the same component keep entirely separate state.

solid answer

~50 s

The state is not in your function and not in a closure — it lives on the fiber, React's internal node for that position in the tree. Before running your component, React sets that fiber as the one currently rendering and resets a cursor into its list of hook records. Each hook call takes the next record in order: `useState` reads its stored value and returns it with its own setter, `useRef` returns its stored box, and so on. When the function returns, the cursor position is checked and the list is stored back on the fiber. Because the storage is per-fiber and positional, rendering `<Counter />` twice creates two fibers and therefore two independent counts, with nothing shared between them. And because the matching is by order rather than by name, the sequence of hook calls has to be identical on every render of that fiber.

code

jsx · 18 lines
jsx
function Counter({ label }) {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      {label}: {count}
    </button>
  );
}

export default function App() {
  // two positions, two fibers, two independent counts
  return (
    <>
      <Counter label="left" />
      <Counter label="right" />
    </>
  );
}

go deeper

for a junior

Be able to say that state does not live in your function — React keeps it for that particular rendered component, which is why two copies of the same component each count independently.

for a middle

Explain the mechanism: React points at the current fiber, walks a list of hook records in call order, and each hook takes the next record. That positional matching is the whole storage model.

for a senior

Draw the consequences a team hits in production: state resets when a position unmounts, setters are stable while values are per-render snapshots, and anything that must survive remounting belongs above the component or outside React.

for a principal

Frame the design tradeoff. Positional storage buys hooks their ergonomics at the cost of a structural constraint on call sites, and you should be able to say when that constraint pushes state out of components and into an explicit store.

## The puzzle ```jsx function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } ``` Every render calls `Counter()` again from the top, and the literal `0` is right there in the source. Yet after three clicks it renders `3`. Nothing in the function body explains that, so the storage must be outside it. ## Where it lives It lives on the **fiber** — React's persistent internal node for this position in the UI tree. Elements are recreated every render and thrown away; fibers are created on mount and reused. The fiber has a field React calls `memoizedState`, and for a function component that field is the head of a singly linked list of *hook records*, one per hook call in the component, in call order. Each record holds whatever that hook needs to persist: for `useState`, the current value plus the queue of pending updates; for `useRef`, the mutable box; for `useMemo`, the cached value and the dependency array from last time. ## How a render walks that list The mechanism is deliberately simple. Right before calling your component function, React records "the fiber currently rendering" in a module-level variable and points a cursor at the first hook record. Each hook call does two things: take the record the cursor points at (creating it on the very first render), then advance the cursor. That is the entire matching rule — position in the call sequence, nothing else. This is why hooks work at all in a plain function with no `this`, no instance, and no way to identify itself. The identity is supplied from outside, by which fiber React happens to be rendering at that moment. ## Consequence one: instances are independent ```jsx <> <Counter /> <Counter /> </> ``` These are two positions, so two fibers, so two hook lists, so two counts. They share the function, the way two objects share a class, and share no data whatsoever. The same reasoning covers a list: ten rows rendered from a `map` have ten fibers and ten independent state slots. If a component appears to "leak" state between instances, the state is not in a hook — it is in a module-level variable or an external store, both of which are genuinely shared. ## Consequence two: state dies with the fiber When a position disappears from the rendered output, its fiber is dropped and every hook record goes with it. There is no cache and no revival: if the same component appears again later, it mounts fresh with the initial values. This is the mechanical reason unmounting resets a form, a toggle or a scroll position, and why anything that must outlive the position has to be held by an ancestor or somewhere outside React entirely. ## Consequence three: order is the identity Because records are matched by position, nothing about a hook record connects it to the variable you assigned it to. Rename `count` to `total` and nothing changes. But skip a hook call on some renders and record two starts answering to what used to be record three, handing one hook another hook's stored value. React detects a change in the number of hooks between renders and throws rather than continue with mismatched state — the failure is loud, not silent corruption. ## Consequence four: the setter is stable, the value is a snapshot The updater function returned by `useState` belongs to the hook record, so it is the same function across renders of that fiber and is safe to pass to children. The *value* is not: it is whatever was read out of the record during the render that produced this closure. Two different renders of the same component close over two different values, which is exactly why reading state inside a long-lived callback can give you an old number. ## What to say, and what not to claim A good interview answer: "Hook state lives on the fiber for that component instance, as a linked list of records matched by call order — so each rendered position gets its own storage, and the state disappears when that position unmounts." Naming `memoizedState` shows you have read the reconciler, and that is a fine thing to show. Do not go on to suggest reading or writing it from application code; the fiber internals are unstable across releases, and everything you actually need is available through the hooks themselves.

  • If hook state is matched by call order, what does React do when the number of hooks changes between renders?
    It throws rather than continuing with mismatched records. React tracks how many hooks a fiber rendered last time, and a shorter or longer sequence means record two would start serving what used to be record three — one hook reading another's value. Failing loudly turns a silent state-corruption bug into an immediate, traceable error.
  • Is the setter function returned by useState stable across renders?
    Yes. It belongs to the hook record on the fiber, so the same function object comes back on every render of that fiber, which makes it safe to pass down to a memoized child or to omit from a dependency array. The state *value* is the opposite: it is a per-render snapshot, so a closure created in one render keeps that render's value.
  • Two sibling components render the same custom hook. Do they share anything?
    No. A custom hook is just a function that calls other hooks, so its records are allocated on whichever fiber is rendering. Each caller gets its own records and its own state. Sharing requires something outside the fiber — context, a parent holding the state, or an external store — which is why "extract to a custom hook" reuses logic but never data.

saying these in an interview costs you the question

  • Claiming state is kept in the component function's closure
  • Thinking hooks are keyed by variable or component name
  • Believing two instances of a component share one state slot
  • Expecting state to survive after the component unmounts
  • Assuming React looks up hooks by the order they are declared in source rather than called

context