Lifting State Up
When two sibling components need to reflect the same changing data, the answer is to move that state up to their nearest common ancestor and pass it down as props. This is called lifting state up, and it is one of the most important patterns in React.
The principle behind it is single source of truth: there should be exactly one place in the component tree that owns any given piece of state. When multiple components need to agree on a value, one of them (or a shared parent) must own it — not each component independently.
The Problem: Sibling State Is Isolated
Imagine a temperature converter with two inputs — one in Celsius and one in Fahrenheit. When the user types in one field, the other should update automatically. Here is the broken first attempt where each input manages its own state:
// ✗ BROKEN — each input has its own state; they cannot stay in sync
function CelsiusInput() {
const [celsius, setCelsius] = useState('')
return (
<label>
Celsius:
<input value={celsius} onChange={e => setCelsius(e.target.value)} />
</label>
)
}
function FahrenheitInput() {
const [fahrenheit, setFahrenheit] = useState('')
return (
<label>
Fahrenheit:
<input value={fahrenheit} onChange={e => setFahrenheit(e.target.value)} />
</label>
)
}
// There is no way for CelsiusInput to update FahrenheitInput's state.
// They live in separate, isolated state bubbles.
function TemperatureConverter() {
return (
<>
<CelsiusInput />
<FahrenheitInput />
</>
)
}Each input keeps its own value, but they know nothing about each other. Typing 100 in the Celsius field cannot magically update the Fahrenheit field, because sibling components cannot directly write to each other's state.
The Fix: Lift State to the Parent
The solution is to remove the local state from both children and instead hold the single source of truth in their shared parent. The parent passes the current value and an onChange handler down as props. The children become controlled components — they display what they receive and report user input upward:
import { useState } from 'react'
// Helper conversions
function toCelsius(f) {
return ((f - 32) * 5) / 9
}
function toFahrenheit(c) {
return (c * 9) / 5 + 32
}
function round(value) {
return Math.round(value * 1000) / 1000
}
// ✓ Each input is now a pure controlled component
function TemperatureInput({ scale, value, onValueChange }) {
return (
<label>
{scale === 'c' ? 'Celsius' : 'Fahrenheit'}:
<input
value={value}
onChange={e => onValueChange(e.target.value)}
/>
</label>
)
}
// ✓ Parent owns the single source of truth
function TemperatureConverter() {
const [celsius, setCelsius] = useState('')
// Fahrenheit is DERIVED — not stored separately
const fahrenheit = celsius !== '' ? round(toFahrenheit(Number(celsius))) : ''
function handleCelsiusChange(value) {
setCelsius(value)
}
function handleFahrenheitChange(value) {
// Convert back to Celsius and store only that
setCelsius(value !== '' ? String(round(toCelsius(Number(value)))) : '')
}
return (
<div>
<TemperatureInput
scale="c"
value={celsius}
onValueChange={handleCelsiusChange}
/>
<TemperatureInput
scale="f"
value={fahrenheit}
onValueChange={handleFahrenheitChange}
/>
{celsius !== '' && (
<p>
{celsius}°C = {fahrenheit}°F
</p>
)}
</div>
)
}Key insight: we only store one value — celsius — in state. Fahrenheit is computed during render. There is no risk of the two values ever diverging because there is only one value to keep track of.
How Data Flows in the Fixed Version
TemperatureConverterownscelsiusin state — this is the single source of truth.It computes
fahrenheitinline during render (derived value, not separate state).Both
TemperatureInputchildren receive their display value and a change handler as props.When the user types in the Fahrenheit input,
handleFahrenheitChangeconverts the value back to Celsius and callssetCelsius, which re-renders the whole tree with the new derived Fahrenheit.The Celsius input re-renders with the stored value unchanged — the two are always in sync.
The Single Source of Truth Principle
Whenever two pieces of data should always agree, they are really just one piece of data seen from two angles. The rule is: store data once, derive everywhere else. Duplication is the enemy:
// ✗ Two sources of truth — prone to desync const [celsius, setCelsius] = useState(0) const [fahrenheit, setFahrenheit] = useState(32) // duplicated! // ✓ One source of truth — Fahrenheit is always correct const [celsius, setCelsius] = useState(0) const fahrenheit = (celsius * 9) / 5 + 32 // derived during render
When to Lift State
Situation | Action |
|---|---|
Two siblings need the same value | Lift state to their shared parent |
A parent needs to know what a child is showing | Lift state to the parent |
A deeply nested component needs state from far above | Consider Context API instead of lifting |
State is only used in one component | Keep it local — do not lift unnecessarily |
The Downside: Prop Drilling
Lifting state works well for one or two levels, but when state is lifted high and must be passed down through many intermediate components that do not use it themselves, you encounter prop drilling:
// Prop drilling across 4 levels — cumbersome
function App() {
const [theme, setTheme] = useState('light')
return <Layout theme={theme} setTheme={setTheme} />
}
function Layout({ theme, setTheme }) {
// Layout doesn't USE theme, just passes it down
return <Sidebar theme={theme} setTheme={setTheme} />
}
function Sidebar({ theme, setTheme }) {
// Sidebar doesn't USE theme either
return <ThemeToggle theme={theme} setTheme={setTheme} />
}
function ThemeToggle({ theme, setTheme }) {
// ThemeToggle is the only one that actually needs theme
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
)
}When to Stop Lifting and Use Context
Use the following heuristic to decide between lifting state and reaching for Context:
1–2 levels of prop passing — lift state. It is explicit and easy to trace.
3+ levels or many unrelated components — consider
useContext. The Context API was designed exactly for this case.Global, low-frequency data (current user, theme, locale, feature flags) — Context is the natural fit.
High-frequency updates (e.g. a value that changes on every keystroke) — be careful with Context because every consumer re-renders. Lifted state only re-renders the sub-tree that actually uses it.
Quick Reference
Sibling components cannot share state directly — they must communicate via a shared parent.
Lift state to the nearest common ancestor of all components that need it.
Store the minimal state, derive everything else. One source of truth eliminates sync bugs.
Pass the value down as a prop and the setter as an
onChange-style callback.When drilling becomes painful across many levels, switch to the Context API.