skip to content

A React component is written as `function Price({ amount }) { const [value, setValue] = useState(amount); ... }` and keeps showing the original number after the parent re-renders with a new `amount`. Why does the useState argument stop having any effect, and what are your options?

level: middleimportance: must knowfreq 78%

answer

  1. the argument is the *initial* value
  2. read once per component instance
  3. two copies, one updates
  4. derive, lift, or own it deliberately
  5. name the prop initialSomething

basics

~20 s

The argument to useState is only the initial value: React reads it on the component instance's first render and ignores it afterwards, so the copy freezes. Either derive from the prop directly, or treat the copy as genuinely separate state with an explicit reset.

solid answer

~50 s

`useState(amount)` sets the *initial* state for that component instance. On every later render React returns the value it is already storing for that hook and throws the argument away, so the copy is frozen at whatever `amount` was the first time the component mounted. That is the props-into-state anti-pattern: two representations of one fact, and only one of them updates. Usually the fix is to stop copying — read `amount` directly, and derive anything you need from it during render, so there is only one source of truth. If the component genuinely owns something separate, such as an editable draft the user can change, then it is real state, not a mirror — and you have to decide explicitly what should happen when the incoming `amount` changes, rather than hoping the initializer will notice. Naming the prop `initialAmount` or `defaultAmount` signals to callers that later changes are intentionally ignored.

code

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

// Frozen: the argument is read once, on this instance's first render
function FrozenPrice({ amount }) {
  const [value] = useState(amount);
  return <span>{value}</span>; // stays at the first amount forever
}

// Derived: one source of truth, recomputed every render
function LivePrice({ amount }) {
  const formatted = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(amount);
  return <span>{formatted}</span>;
}

// Genuinely local draft: the prop is only a seed, so say so in its name
function AmountEditor({ initialAmount, onSubmit }) {
  const [draft, setDraft] = useState(initialAmount);
  return (
    <input
      value={draft}
      onChange={(e) => setDraft(e.target.value)}
      onBlur={() => onSubmit(draft)}
    />
  );
}

go deeper

for a junior

Know that the value passed to useState is only the initial state and is ignored on later renders of the same component. Be able to say that displaying a prop needs no state at all.

for a middle

Explain that state is stored per component instance and survives re-renders, so the argument is consulted once at mount. Walk through the three options — derive, lift, or own it deliberately — and why a syncing effect is not one of them.

for a senior

Show how this bug behaves in a reused list row, where a stale mirror shows another entity's data, and argue the review rule: any useState(props.x) must justify itself or be deleted. Cover what happens to a dirty draft when the source changes.

for a principal

Own the API contract: a prop that is only a seed must be named initial*/default* so every call site sees it, and a controlled/uncontrolled split has to be a documented decision for shared components rather than something each consumer discovers by hitting the bug.

## What `useState(x)` actually promises `useState` returns the state React is currently holding for that hook slot on that component instance, plus a setter. The argument is consulted exactly once — on the instance's first render — to seed the slot. React does not compare it against later values, and there is no mechanism by which changing the argument reaches the stored state. (Passing a function, `useState(() => expensive())`, only changes *how* the initial value is produced; it is still consulted once.) So state belongs to the mounted component instance, not to the render. Re-rendering with new props re-runs the function body, evaluates `useState(amount)` again, and discards the argument. The state survives; the argument does not. ## Why this is the classic interview bug The copy is a second representation of a fact the parent already owns. The moment the parent updates `amount`, the two disagree — and unlike a normal bug, nothing errors. The component just displays a number that was correct a while ago. In a list where a row component is reused for a different record, it displays *another entity's* value, which is worse than stale. Candidates who half-know the problem reach for a synchronising effect: ```jsx useEffect(() => { setValue(amount); }, [amount]); ``` This makes the number update, but it keeps the duplication and adds a lag: React renders and commits with the old value, then the effect fires and schedules a second render. Any local edit the user made is silently blown away whenever the parent re-renders with a new `amount`, and now there are two writers to one variable. It converts a visible bug into an intermittent one. ## Option 1 — derive, don't copy (the usual answer) If the component only ever displays or transforms the prop, delete the state: ```jsx function Price({ amount }) { const formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(amount); return <span>{formatted}</span>; } ``` The prop is the single source of truth, the derived string is recomputed each render, and staleness is structurally impossible. Most real occurrences of this bug are this case: the state was never needed. ## Option 2 — lift the change upward If the component needs to *modify* the value and the parent needs to know, the value belongs to the parent. Take `amount` and an `onAmountChange` callback as props and hold no local copy. There is still one owner; the child just reports intent. ## Option 3 — accept that it is genuinely separate state Sometimes the local value is not a mirror at all but new information: a draft the user is editing before saving, or a snapshot deliberately frozen at a moment in time (the price quoted when the dialog opened, which must not shift underneath the user). Then a local `useState` seeded from the prop is correct, and the design question becomes: what should happen when the incoming prop changes while the draft is dirty? Overwrite and lose the edit, keep the edit and show a conflict, or discard the draft because the component is now editing a different entity — the last of which means remounting the component so its state starts fresh. Whatever you choose, it must be a deliberate decision in the code, not an accident of the initializer. ## Make the contract visible in the name When a prop really is only a seed, name it so: `initialAmount`, `defaultValue`, `defaultOpen`. This is the convention the DOM itself uses (`defaultValue` versus `value`) and React follows it for uncontrolled inputs. A prop named `amount` that is silently ignored after mount is a trap for every future caller; a prop named `initialAmount` documents the behaviour at the call site. ## The diagnostic sequence to say out loud 1. Is this value derivable from the prop? Then compute it during render and hold no state. 2. Does the parent need to know about changes? Then lift the value and take a callback. 3. Is it genuinely independent local information? Then it is state — name the prop `initial*`, and define explicitly what happens when the source changes. 4. Never reconcile a mirror with an effect whose only job is `setState(prop)`.

  • A colleague fixes it with `useEffect(() => setValue(amount), [amount])`. What is still wrong?
    The duplication survives. The component renders and commits with the old value before the effect corrects it, so there is a stale frame plus an extra render, and any local edit is silently overwritten whenever the parent re-renders. Two writers now own one variable. Deleting the state removes the whole class of bug instead of masking it.
  • Does passing a function, as in `useState(() => compute(amount))`, make the initializer re-run when `amount` changes?
    No. The lazy form only changes *how* the initial value is produced — it avoids running `compute` on every render — but React still consults it once, on the instance's first render. It is a performance affordance for expensive initialisation, not a synchronisation mechanism.
  • When is copying a prop into state actually the right design?
    When the local value is new information rather than a mirror: an editable draft the user changes before saving, or a value deliberately frozen at a moment in time so it does not shift underneath them. Then name the prop `initialX` or `defaultX`, and define explicitly what happens when the source changes mid-edit.

saying these in an interview costs you the question

  • useState re-reads its argument whenever props change
  • Add a useEffect to sync the prop into the state
  • Props are read-only, so you must copy them into state first
  • The state is stale because a dependency array is missing
  • Renaming the prop to initialAmount changes the runtime behaviour

context