State desync

Two sources of truth drift apart. A parent and child both store the same selected item. When updates happen independently, ownership becomes ambiguous and the UI lies.

Break the invariant: duplicated selection state

Try: click an item in the child list, then click "Parent: Select next". The child keeps its own local selection and can drift out of sync.

Buggyduplicated selection state
Parent selectedId
a
Last update source
Parent view
Selected: Alpha (a)
This panel always reflects the parent state.
Child list (has its own local state)
Parent says: a · Child local: a

Detected issue

Detected issue

Status: Invariant intact
How to trigger it
  • Click an item in the child list.
  • Then click “Parent: Select next”.
Click "Parent: Select next" after selecting in the child.
Why this breaks
useState(prop) runs only on mount — it does not react to subsequent prop changes. If the parent updates later, the child’s local copy becomes stale. You now have duplicated state — two owners — and the UI can lie.

Reference (correct mental model)

Buggy pattern

// ❌ duplicated source of truth const [selectedId, setSelectedId] = useState("a"); // parent function Child({ selectedIdFromParent }) { // Initialized once. Does NOT track parent changes. const [localSelectedId] = useState(selectedIdFromParent); // initialized once // parent changes later -> child stays stale }

Correct pattern

// ✅ single owner (parent), controlled child const [selectedId, setSelectedId] = useState("a"); function ControlledList({ selectedId, onSelect }) { return items.map(it => ( <button key={it.id} aria-pressed={it.id === selectedId} onClick={() => onSelect(it.id)} > {it.label} </button> )); } // Parent passes selectedId and setSelectedId as onSelect

Fix: lift state up (single source of truth)

The parent owns selectedId. The list becomes controlled: it receives selectedId and reports intent via onSelect.

Fixedsingle source of truth (controlled)
Parent selectedId
a
Last update source
Parent view
Selected: Alpha (a)
This panel always reflects the parent state.
Controlled list (no duplicated state)
Single source of truth: a

Key takeaways

  • Duplicating the same truth in two states creates drift risk.
  • useState(prop) runs only on mount — it does not track subsequent prop changes.
  • Fix by choosing one owner and making others controlled/derived.

Approach & tradeoffs

Approach

Lift selection state to the parent. Make the child controlled (selectedId + onSelect). The parent becomes the single owner; children render derived UI.

Tradeoffs

  • Prop wiring increases as the tree grows.
  • If many consumers need the state, consider Context or a store (Zustand/Redux).
Common pitfall

Trying to ‘sync’ local state with props via effects as a default pattern. It often adds complexity and still misses edge cases.

Fix: Prefer controlled components or derive UI from props. Only keep local state when it’s truly local and not shared truth.