Skip to main content

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

tsx
// ❌ 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

tsx
// ❌ 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

tsx
// ❌ 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

tsx
// ❌ 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 todos array lifted to App
  • Next.js search pages: query state lifted above both the search input and results list
  • Login forms: showPassword toggle and the password field 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

tsx
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 immediately

One component, one source of truth. Both inputs read from celsius and write back through converter functions.

Intermediate: login form with shared state

tsx
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 level

The 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?