Imagine two components โ a temperature input in Celsius and one in Fahrenheit โ that need to stay in sync. If each manages its own separate state, they have no way to know when the other one changes. The fix: neither component owns the temperature itself โ their shared parent does, passing it down to both.
function TemperatureInput({ label, value, onChange }) {
return (
<label>
{label}:
<input value={value} onChange={(e) => onChange(e.target.value)} />
</label>
);
}
function Converter() {
const [celsius, setCelsius] = useState("");
const fahrenheit = celsius ? (celsius * 9) / 5 + 32 : "";
return (
<>
<TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} />
<TemperatureInput label="Fahrenheit" value={fahrenheit} onChange={() => {}} />
</>
);
}Neither TemperatureInput holds its own state anymore โ Converter owns the single source of truth (celsius) and derives everything else from it, passing both the value and an update function down to each child.
The general rule: if two or more components need to reflect the same changing data, that data's state should live in their closest common parent, not duplicated separately in each component. This single principle resolves the majority of "my components are out of sync" bugs beginners run into.
Giving two sibling components their own separate useState for what should be the same underlying value guarantees they'll eventually get out of sync โ updating one has no way to affect the other. If you find yourself needing to keep two pieces of state "in sync" manually, that's the signal to lift the state up instead.
Build an accordion with two sections where only one can be open at a time โ this requires lifting the "which section is open" state up to a shared parent.
function Section({ title, isOpen, onToggle }) {
return (
<div>
<button onClick={onToggle}>{title}</button>
{isOpen && <p>Content for {title}</p>}
</div>
);
}
function Accordion() {
const [openSection, setOpenSection] = useState(null);
return (
<>
<Section title="Section A" isOpen={openSection === "A"} onToggle={() => setOpenSection("A")} />
<Section title="Section B" isOpen={openSection === "B"} onToggle={() => setOpenSection("B")} />
</>
);
}
Q Does lifting state up mean my child components lose all their own state?
No โ a child can still hold state that's genuinely local to it (like whether a tooltip is hovered). Only state that needs to be shared or synchronized across multiple components needs to move up.
Q How far up should I lift state โ always to the very top App component?
No โ only as far up as the closest common parent of the components that need to share it. Lifting state higher than necessary makes that top component bloated and forces data through components that don't actually need it.