Lifting State Up
When two or more components need to reflect the same changing data, the cleanest solution in React is to move that state up to their closest common ancestor. This pattern is called lifting state up. Instead of each component holding its own copy of the data, the shared value lives in one parent, which passes it down as props and exposes setters as callbacks. The result is a single source of truth that keeps every child perfectly in sync.
Why state ends up in the wrong place
It is natural to declare state inside the component that uses it. But the moment a sibling component needs to read or change that same value, local state becomes a problem: siblings cannot talk to each other directly in React. Data flows down the tree through props, never sideways. So if two siblings must agree on a value, the value has to live somewhere they can both see it — their common parent.
Rule of thumb: state should live at the lowest common ancestor of every component that reads or writes it. No higher, no lower.
Passing state down, lifting events up
Lifting state has two halves that work together:
- State flows down as props. The parent owns the value and hands the current value to each child.
- Events flow up as callbacks. Children cannot mutate the parent’s state directly, so the parent passes down setter functions. A child calls the callback, the parent updates its state, and React re-renders both children with the new value.
This round trip — child calls a function, parent updates state, new props flow back down — is often called inverse data flow. The data still only flows one way (down), but a child can request a change by invoking a callback the parent gave it.
Worked example: two synced inputs
Imagine a temperature converter with a Celsius field and a Fahrenheit field. Typing in either one should instantly update the other. If each input owned its own state, they could drift apart. By lifting the temperature into the parent, both inputs always reflect a single value.
import { useState } from "react";
function TemperatureInput({ scale, value, onChange }) {
const label = scale === "c" ? "Celsius" : "Fahrenheit";
return (
<fieldset>
<legend>Enter temperature in {label}:</legend>
<input
type="number"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</fieldset>
);
}
function toCelsius(f) {
return ((f - 32) * 5) / 9;
}
function toFahrenheit(c) {
return (c * 9) / 5 + 32;
}
function tryConvert(value, convert) {
const input = parseFloat(value);
if (Number.isNaN(input)) return "";
const output = convert(input);
return (Math.round(output * 1000) / 1000).toString();
}
export default function Calculator() {
const [temperature, setTemperature] = useState("");
const [scale, setScale] = useState("c");
const celsius =
scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit =
scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
value={celsius}
onChange={(value) => {
setScale("c");
setTemperature(value);
}}
/>
<TemperatureInput
scale="f"
value={fahrenheit}
onChange={(value) => {
setScale("f");
setTemperature(value);
}}
/>
<p>
{tryConvert(temperature, scale === "c" ? toCelsius : toFahrenheit) === ""
? "Enter a number to see the conversion."
: "Both fields stay in sync."}
</p>
</div>
);
}
Here the Calculator parent holds two pieces of state: the raw temperature value and which scale the user last edited. Each TemperatureInput is fully controlled — it receives its displayed value as a prop and reports edits through onChange. Neither input owns the data, so they can never disagree.
Output:
Typing "100" into the Celsius field instantly shows "212" in Fahrenheit.
Clearing a field clears both.
What to keep where
A useful way to decide what becomes state versus what is computed:
| Concern | Where it lives | Why |
|---|---|---|
| The raw value the user typed | State in the parent | It is the single source of truth |
| Which input was last edited | State in the parent | Determines conversion direction |
| The converted value shown in the other field | Derived during render | It is computed from state, not stored |
| Each input’s current display value | Props passed from parent | Children only render what they are given |
Notice we store the minimal state and derive everything else. Storing both temperatures separately would create two sources of truth and force you to keep them manually in sync — the exact bug lifting state is meant to prevent.
Lifting too high
Lifting state has a cost: every change re-renders the owning parent and all of its children. If you lift state higher than necessary, unrelated parts of the tree re-render and prop-drilling gets tedious. Lift only as high as the closest common ancestor. When state truly needs to be shared across many distant components, reach for Context or a state library instead of threading props through a dozen layers.
Gotcha: if you find yourself passing the same prop through several intermediate components that do not use it, you have likely lifted too high. Consider Context.
Best practices
- Identify every component that reads or writes a value, then place the state at their lowest common ancestor.
- Keep state minimal: store only raw values and derive the rest during render.
- Pass the current value down as a prop and pass a setter or handler down as a callback.
- Make shared inputs fully controlled so the parent stays the single source of truth.
- Avoid duplicating the same data into multiple
useStatehooks across siblings. - Do not lift state higher than the closest common ancestor; reach for Context when props would travel through many layers.