Lifting state up in React
Lifting state up - a React pattern where shared state moves to the closest common parent of the components that need it, then flows back down as props.
Theory
TL;DR
- Two siblings can't talk directly. The parent holds the data and passes it down.
- Analogy: two kids, one thermostat. The parent holds the dial and adjusts it on request.
- When parent state changes, all children re-render with the same fresh value. Local states drift apart.
- Lift when 2+ siblings need the same data, or one input should drive another.
Quick example
// ❌ Two inputs, two local states. They never sync.
function CelsiusInput() {
const [temp, setTemp] = useState(0);
return <input value={temp} onChange={e => setTemp(+e.target.value)} />;
}
function FahrenheitInput() {
const [temp, setTemp] = useState(32); // separate state, drifts apart
return <input value={temp} onChange={e => setTemp(+e.target.value)} />;
}
// ✅ Parent owns state. Children are controlled.
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
return (
<>
<input value={celsius} onChange={e => setCelsius(+e.target.value)} />
<input
value={Math.round(celsius * 9 / 5 + 32)}
onChange={e => setCelsius((+e.target.value - 32) * 5 / 9)}
/>
</>
);
}
// Change Celsius → Fahrenheit updates instantly. And vice versa.Key difference
After lifting, child components become controlled: they read from props and call parent handlers on change. Their local useState disappears. The parent re-renders and pushes fresh values to all children at the same time, so they stay in sync automatically.
When to use
- 2+ siblings need the same data → lift to their closest common parent
- One input drives another (Celsius/Fahrenheit) → lift and compute derived values in parent
- Form fields need to validate together → lift to the form container
- A list and a filter read from the same array → lift the array
Keep state local if only one component uses it. If you're passing props 3+ levels deep just to share state, that's prop drilling. At that point, Context or a state manager is the better call.
How React handles it internally
When setState fires in a parent, React schedules a re-render of that component and its subtree. Children receive fresh props in the render phase. If a prop value changed, React updates the DOM with input.value = newValue. All children update in the same pass, which is why they can't go out of sync.
Common mistakes
Mixing local state with the incoming prop
// ❌ Local state overrides the prop. Changes never reach the parent.
function Child({ value, onChange }: { value: number; onChange: (v: number) => void }) {
const [local, setLocal] = useState(0); // fights the prop!
return <input value={local} onChange={e => setLocal(+e.target.value)} />;
}
// ✅ Controlled: use the prop directly
function Child({ value, onChange }: { value: number; onChange: (v: number) => void }) {
return <input value={value} onChange={e => onChange(+e.target.value)} />;
}In practice, this is the trickiest bug from this pattern. You pass a value as a prop, the child also has a local useState, and changes never reach the parent. React DevTools shows the prop updating while the input stays frozen.
Lifting too high
// ❌ App re-renders on every keypress inside a login form
<App formData={formData} setFormData={setFormData}>
<LoginForm />
</App>Lift only to the closest common parent. LoginPage is the right level, not App. Unrelated components shouldn't re-render because of a form field.
Mutating state instead of replacing it
// ❌ Push mutates the array. React sees the same reference. No re-render.
const addItem = () => { items.push('new'); setItems(items); };
// ✅ Create a new array
const addItem = () => setItems([...items, 'new']);Real-world usage
- React docs temperature converter: the original lifting state up walkthrough uses Celsius/Fahrenheit exactly as above
- TodoMVC: filter buttons and the todo list share the
todosarray lifted toApp - Next.js search pages: query state lifted above both the search input and results list
- Login forms:
showPasswordtoggle and thepasswordfield share state in the form parent
Follow-up questions
Q: Why not use Redux or Context for everything?
A: Redux adds boilerplate (actions, reducers, store setup) that isn't worth it for two siblings sharing data. Lift first. Reach for Context at 5+ consumers or 3+ nesting levels. Redux makes sense for app-wide state shared across many unrelated components.
Q: How does a child send data up to the parent?
A: Pass a callback prop: onValueChange(data). The child calls it, the parent updates its state. That is the whole mechanism.
Q: Does lifting state cause performance issues?
A: Rarely. React batches state updates inside event handlers. If a specific subtree re-renders too often, wrap stable children in React.memo. Measure first with React DevTools Profiler before optimizing anything.
Q: (Senior) When does lifting become prop drilling? Walk through the migration path.
A: Around 10+ fields or 3+ nesting levels. Extract a useForm hook returning { values, updateField }. Wrap the subtree in <FormContext.Provider> and consume with useContext. State still lives in one place, but components access it without long prop chains.
Examples
Basic: temperature converter
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
const toFahrenheit = (c: number) => Math.round(c * 9 / 5 + 32);
const toCelsius = (f: number) => (f - 32) * 5 / 9;
return (
<div>
<label>
Celsius:
<input
type="number"
value={celsius}
onChange={e => setCelsius(+e.target.value)}
/>
</label>
<label>
Fahrenheit:
<input
type="number"
value={toFahrenheit(celsius)}
onChange={e => setCelsius(toCelsius(+e.target.value))}
/>
</label>
</div>
);
}
// Type 100 in Celsius → Fahrenheit shows 212 immediately
// Type 32 in Fahrenheit → Celsius shows 0 immediatelyOne component, one source of truth. Both inputs read from celsius and write back through converter functions.
Intermediate: login form with shared state
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
showPassword: false,
});
const updateField = (field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({ ...formData, [field]: e.target.value });
return (
<form>
<input
value={formData.email}
onChange={updateField('email')}
placeholder="Email"
/>
<input
type={formData.showPassword ? 'text' : 'password'}
value={formData.password}
onChange={updateField('password')}
/>
<label>
<input
type="checkbox"
checked={formData.showPassword}
onChange={e => setFormData({ ...formData, showPassword: e.target.checked })}
/>
Show password
</label>
</form>
);
}
// Toggle checkbox → password field switches between text and password type
// All three fields share one state object at the form levelThe checkbox and the password input are siblings. Without lifting, the checkbox couldn't control the input type. The form component owns everything, so the coordination is straightforward.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.