Lifting State Up: Share Data Between Siblings Without Chaos
Learn how to move state to a common parent so sibling components can stay simple and in sync.
Two siblings needed the same data: an input collecting a name, a display showing it. When each component owns its own copy, the copies drift out of sync and you end up passing too many callbacks around to reconcile them. Lifting state up is the fix I reach for first: move the state to the closest common parent of everyone who needs it, then pass it down as props. That single source of truth keeps the siblings in agreement, and each child gets to stay simple.
An input that talks to no one
In the broken version, NameInput manages its own name and NameDisplay has no way to reach it:
import { useState } from "react";
// NameInput owns its own state — siblings can't share it
function NameInput() {
const [name, setName] = useState("");
return (
<div>
<label>Enter your name:</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
// NameDisplay doesn't know the current name
function NameDisplay() {
return (
<div>
<p>Hello, {}!</p>
</div>
);
}
function App() {
return (
<div>
<h1>Lifting State Up Example</h1>
<NameInput />
<NameDisplay />
</div>
);
}
export default App;
The input holds state nobody else can see. The display renders Hello, {}! no matter what you type. That’s the drift problem in miniature, and the logic gets duplicated the moment another component needs the name too.
The parent holds the name
Lift name into App and hand it down to both children:
import { useState } from "react";
function NameInput({ name, setName }) {
return (
<div>
<label>Enter your name:</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
function NameDisplay({ name }) {
return (
<div>
<p>Hello, {name}!</p>
</div>
);
}
function App() {
const [name, setName] = useState("");
return (
<div>
<h1>Lifting State Up Example</h1>
<NameInput name={name} setName={setName} />
<NameDisplay name={name} />
</div>
);
}
NameInput becomes a controlled component taking name and setName, and NameDisplay just renders what it receives. The steps are mechanical: strip the local state out of NameInput, add the useState call to App, then pass name and setName to the input and name to the display. The data flow is explicit now, parent to children, easy to trace. Each component has one job, input or display, and both stay in sync because there’s only one copy of the name.
When not to keep lifting
Lifting has its limits. If many deeply nested components need the same value, use Context instead of drilling props through every level. If the parent grows large, extract presentational children or move the logic into custom hooks. For form inputs, control both value and onChange; miss one and the UI drifts from the state.