Lifting state up and prop drilling
Two sibling components can't share state directly — there's no mechanism for it. The fix is moving the state to their common parent, which works cleanly right up until that parent is many levels removed from where the data is actually needed.
4 min read
The problem: two siblings that need to share one piece of state
function TemperatureInput() {
const [value, setValue] = useState(""); // this component's OWN state — Fahrenheit
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
function BoilingWarning() {
// has no way to know what TemperatureInput's current value actually is —
// there is no direct sibling-to-sibling communication channel in React at all
}TemperatureInput and BoilingWarning, as siblings, have no direct way to share data — React simply provides no mechanism for one sibling to read another's state, since (from the props-and-data-flow lesson) data only ever flows from parent to child. If BoilingWarning needs to know the current temperature to decide whether to show a warning, the state genuinely cannot live inside TemperatureInput at all.
Lifting state up: moving it to the nearest common ancestor
function TemperatureInput({ value, onChange }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
function BoilingWarning({ value }) {
return Number(value) >= 100 ? <p>Warning: boiling!</p> : null;
}
function Converter() {
const [temperature, setTemperature] = useState(""); // state now lives in the shared parent
return (
<>
<TemperatureInput value={temperature} onChange={setTemperature} />
<BoilingWarning value={temperature} />
</>
);
}Lifting state up means moving the state out of the component that originally "owned" it and into the nearest shared ancestor of every component that actually needs it — Converter now owns temperature, and passes it down to both children: TemperatureInput as a controlled value plus an onChange callback (the exact pattern from the controlled-inputs section of the events lesson), BoilingWarning as a plain read-only prop. Both siblings now stay in sync automatically, because they're both just reflecting the same single source of truth, rather than each independently trying to track their own copy.
Prop drilling: the real cost once the tree gets deeper
function App() {
const [user, setUser] = useState({ name: "Ada" });
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />; // Layout doesn't use user at all — just passing it through
}
function Sidebar({ user }) {
return <UserMenu user={user} />; // Sidebar doesn't use it either
}
function UserMenu({ user }) {
return <p>{user.name}</p>; // finally, actually used, four levels down
}Prop drilling is exactly this: passing a prop through several intermediate components that don't actually use it themselves, purely so it can reach a component several levels deeper that does. This isn't "wrong" for a shallow tree — it's simple, explicit, and easy to trace (the entire point of one-way data flow, from the earlier lesson) — but it becomes genuinely painful once the tree gets deep enough, or once several unrelated pieces of state all need drilling through the same intermediate components, each of which now has to accept and forward props it has zero actual use for itself.
Where lifting state stops being the right tool
// Lifting state up requires moving it to a common ancestor —
// but what if that "common ancestor" is the entire app's root,
// and dozens of components at every depth need the same piece of data?
function App() {
const [theme, setTheme] = useState("dark"); // needed by nearly EVERY component, at every depth
return <Everything theme={theme} />; // drilling this through the whole tree is genuinely painful
}Lifting state up is the correct first move for genuinely localized sharing — two or three related components, reasonably close together in the tree. It stops being the right tool once a piece of data (a logged-in user, a theme, a language preference) needs to reach many components scattered at many different depths, since the "common ancestor" ends up being close to the app's root, and drilling through every intermediate layer becomes real, ongoing maintenance overhead for components that have nothing to do with the data itself. This is exactly the gap the next lesson's Context API exists to close — a way to make a value available to a whole subtree without manually drilling it through every layer in between.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why can't two sibling components share state directly with each other in React?
2. What does 'lifting state up' actually mean?
3. What is 'prop drilling,' and why does it become a real problem as a component tree gets deeper?