AcademyReact
Lifting State Up and Prop Drilling
Why two components that need to stay in sync must share state owned by their closest common ancestor, and when passing that state down several layers becomes a real problem.
3 min readUpdated 3 exercises
The bug: state that should be shared, isn't
function TemperatureInput() {
const [value, setValue] = useState('');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
function Panel() {
return (
<>
<TemperatureInput />
<TemperatureInput />
</>
);
}Two inputs, each with its own useState call. Type in one and the other does not move — of course it doesn't, each is its own independent closure over its own state variable. useState scopes state to the component instance that called it, and there are two instances here. If the two inputs are meant to represent the same temperature in different units, nothing here connects them.
The fix: lift the state to the closest common ancestor
function Panel() {
const [celsius, setCelsius] = useState('');
return (
<>
<TemperatureInput
value={celsius}
onChange={setCelsius}
/>
<TemperatureInput
value={toFahrenheit(celsius)}
onChange={(f) => setCelsius(toCelsius(f))}
/>
</>
);
}
function TemperatureInput({ value, onChange }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}The state moves up to Panel, the one component that is an ancestor of both inputs, and TemperatureInput stops owning any state of its own — it just renders whatever value it's handed and calls onChange when the user types. Now there is exactly one source of truth, and both inputs are views onto it. This is "lifting state up": find the nearest component that is an ancestor of everything that needs to read or write the value, and put the state there instead.
The rule for where to put state in React is the same rule every time: state lives at the lowest common ancestor of everything that needs it, no higher and no lower. Too low, and siblings can't share it. Too high, and every re-render of that state re-renders parts of the tree that never asked to care about it.
Prop drilling: the cost of passing it down
Once state is lifted, everything below it that needs the value gets it as a prop. That's fine for one or two levels. It gets uncomfortable when the value has to pass through components that never use it themselves, purely to reach a child several layers down:
<Page user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user} />
</Sidebar>
</Layout>
</Page>Page, Layout and Sidebar each take user only to forward it — none of them reads it. This is prop drilling: correct, typed, easy to follow for a level or two, and increasingly annoying to maintain as more layers and more drilled props pile up. Renaming the prop, or adding a second one that needs the same trip, means touching every component in between even though none of them cares about the value.
What actually signals a problem
Two or three layers of drilling for one prop is not automatically wrong — it's often the simplest thing that works, and simplest should stay the default. The real signal is a prop threaded through four or more components that do nothing with it, or several unrelated props all making the same long trip because they're all needed by the same distant descendant. At that point two escapes exist:
- Context — skip the intermediate layers entirely for values that are broadly needed (see the Context page), at the cost of every consumer re-rendering when the value changes.
- Composition — pass components as
childrenor as props instead of drilling the data itself, soLayoutreceives an already-built<Sidebar>element instead of the piecesSidebarneeds to build itself, and never has to knowuserexists.
What to say in an interview
State lives at the lowest common ancestor of the components that need it — no lower, or siblings can't share it; no higher, or unrelated parts of the tree re-render for no reason. Prop drilling is the ordinary, acceptable cost of that rule for a level or two; it becomes worth fixing only once a prop is passing through components that do nothing with it themselves, and the fix is Context or composition, not moving the state even higher.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- useToggle HookA shared boolean two components must agree on — the smallest real case for lifting state up.ChallengeSolve · EasyChallengeSolve · Easy
- Optimistic UpdateProA parent-owned piece of state that several children read and must roll back together — lifted state under real pressure.ChallengeSolve · MediumChallengeSolve · Medium
- State Management · Context & ReducersQuestions on where state should live and how it flows down through a component tree.QuizTake · MediumQuizTake · Medium
Read next
React Context and When to Use It
Context skips prop drilling for broadly needed values, but every consumer re-renders on any change to it — the cost most people miss until it shows up in a profiler.
3 min
Choosing Between useReducer and useState
useReducer is useState generalized: a pure reducer plus dispatch instead of a raw setter, and when that trade is worth making.
3 min