Runnable lesson · press Try it on the examples to edit and run them here.
Sometimes two components must show the same data and stay in sync: two inputs for one value, a list and a detail panel, a filter and a result list. Neither can own the state, because they are siblings and props only flow downward. The fix is to lift the state up to their closest common parent.
The problem
Imagine two inputs, Celsius and Fahrenheit. Changing one must update the other. If each input keeps its own state, they cannot know about each other.
The solution in three steps
- Remove the state from the child components.
- Add it to the closest common parent.
- Pass the value down as props, and pass change handlers down too, so the children can ask the parent to update.
function toF(c) { return (c * 9) / 5 + 32; }
function toC(f) { return ((f - 32) * 5) / 9; }
const round = (n) => String(Math.round(n * 100) / 100);
// A "dumb" input: it has no state, it only shows its value and reports changes
function TemperatureInput({ label, value, onChange }) {
return (
<label style={{ display: "block", marginBottom: 8 }}>
{label}{" "}
<input aria-label={label} value={value} onChange={(e) => onChange(e.target.value)} />
</label>
);
}
function App() {
// The single source of truth lives here, in the common parent
const [scale, setScale] = React.useState("c");
const [text, setText] = React.useState("");
const number = parseFloat(text);
const valid = !Number.isNaN(number);
const celsius = scale === "c" ? text : valid ? round(toC(number)) : "";
const fahrenheit = scale === "f" ? text : valid ? round(toF(number)) : "";
return (
<div>
<TemperatureInput label="Celsius" value={celsius} onChange={(v) => { setScale("c"); setText(v); }} />
<TemperatureInput label="Fahrenheit" value={fahrenheit} onChange={(v) => { setScale("f"); setText(v); }} />
</div>
);
}
What happens: the parent stores one value and which scale the user typed it in. It derives the other value, so the two inputs can never disagree (Derived State). The children are controlled by props, and communicate through callbacks (Passing Functions as Props).
Another example: select an item in a list
A list and a detail panel need to agree on which item is selected:
const FRUITS = [
{ id: 1, name: "Apple", note: "Crisp and tart" },
{ id: 2, name: "Mango", note: "Sweet and juicy" },
];
function FruitList({ selectedId, onSelect }) {
return (
<ul>
{FRUITS.map((f) => (
<li key={f.id}>
<button onClick={() => onSelect(f.id)} aria-pressed={f.id === selectedId}>{f.name}</button>
</li>
))}
</ul>
);
}
function FruitDetail({ fruit }) {
if (!fruit) return <p>Select a fruit</p>;
return <p>You chose: {fruit.name}. {fruit.note}</p>;
}
function App() {
const [selectedId, setSelectedId] = React.useState(null); // lifted state
const selected = FRUITS.find((f) => f.id === selectedId); // derived
return (
<div>
<FruitList selectedId={selectedId} onSelect={setSelectedId} />
<FruitDetail fruit={selected} />
</div>
);
}
Store the id, not a copy of the object. Then there is only one source of truth, and the detail panel finds the current object each render.
How to decide where state lives
- Identify every component that uses the state.
- Find their closest common parent in the tree.
- Put the state there (or higher, if more components will need it later).
State should live as low as possible while still covering all the components that need it. Lifting it higher than necessary makes more components re-render and spreads knowledge across the app.
When lifting becomes painful
If you lift state up through many layers, passing props at every level (prop drilling), consider:
- Composition: pass the child components themselves as
childrenor as props, so intermediate layers do not need the data (Component Composition). - Context: for data needed deep in the tree (Context API).
- A state library for large, app-wide client state.
Controlled and uncontrolled components
A component that gets its value and onChange from props (like TemperatureInput) is controlled by its parent. One that keeps its own state is uncontrolled. Lifting state up turns uncontrolled components into controlled ones. Good reusable components often support both: controlled when the parent passes value, uncontrolled otherwise (Component Reusability).
Key points
- When siblings need the same data, move the state to their closest common parent.
- Pass the value down as a prop and change handlers down as props.
- Keep state as low as possible; derive everything else.