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
Tradeoffs
- Prop wiring increases as the tree grows.
- If many consumers need the state, consider Context or a store (Zustand/Redux).
Common pitfall