Lifting State Up in React – Parent-Child State Management (2025 Guide)
In React, data flows in one direction — from parent to child through props.
Sometimes, two or more sibling components need to share the same state.
Instead of duplicating state in each component, React encourages lifting state up to the closest common ancestor.
Lifting state up ensures that the shared state lives in one place, making your app predictable and easier to maintain.
Why Lift State Up?
Avoid duplicated state in multiple components
Synchronize related components that depend on the same data
Maintain single source of truth for consistent UI
Common examples:
Two input fields showing the same data
Tabs that need to communicate selected state
Form components that update a shared summary
Example: Two Inputs Sharing the Same State
We’ll create a temperature converter with Celsius and Fahrenheit inputs. Both inputs must stay in sync.
Step 1: Create Child Components
function CelsiusInput({ temperature, onTemperatureChange }) {
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input value={temperature} onChange={(e) => onTemperatureChange(e.target.value)} />
</fieldset>
);
}
function FahrenheitInput({ temperature, onTemperatureChange }) {
return (
<fieldset>
<legend>Enter temperature in Fahrenheit:</legend>
<input value={temperature} onChange={(e) => onTemperatureChange(e.target.value)} />
</fieldset>
);
}
Each input component receives state and a callback function as props.
Step 2: Lift State Up to Parent Component
import React, { useState } from "react";
import CelsiusInput from "./CelsiusInput";
import FahrenheitInput from "./FahrenheitInput";
function TemperatureConverter() {
const [celsius, setCelsius] = useState("");
const [fahrenheit, setFahrenheit] = useState("");
const handleCelsiusChange = (value) => {
setCelsius(value);
setFahrenheit(((parseFloat(value) * 9) / 5 + 32).toFixed(2));
};
const handleFahrenheitChange = (value) => {
setFahrenheit(value);
setCelsius(((parseFloat(value) - 32) * 5 / 9).toFixed(2));
};
return (
<div>
<CelsiusInput temperature={celsius} onTemperatureChange={handleCelsiusChange} />
<FahrenheitInput temperature={fahrenheit} onTemperatureChange={handleFahrenheitChange} />
</div>
);
}
export default TemperatureConverter;
How It Works
The state lives in the parent (
TemperatureConverter).Child components receive state and update functions as props.
When a child changes the value:
It calls the callback (
onTemperatureChange)The parent updates state
Both children receive updated props → stay in sync
This is lifting state up — moving state from children to the parent so it can be shared.
Key Principles
Always lift state to the closest common ancestor of all components that need it.
Pass state and updater functions as props to children.
Avoid keeping the same state in multiple places.
Another Example: Checkbox Group
function Checkbox({ label, checked, onChange }) {
return (
<label>
<input type="checkbox" checked={checked} onChange={onChange} />
{label}
</label>
);
}
function CheckboxGroup() {
const [checkedItems, setCheckedItems] = useState({ apple: false, banana: false });
const handleChange = (item) => {
setCheckedItems({ ...checkedItems, [item]: !checkedItems[item] });
};
return (
<div>
<Checkbox label="Apple" checked={checkedItems.apple} onChange={() => handleChange("apple")} />
<Checkbox label="Banana" checked={checkedItems.banana} onChange={() => handleChange("banana")} />
</div>
);
}
State (
checkedItems) lives in parentChildren just trigger updates via props
This prevents inconsistent states across checkboxes
Best Practices
Only lift state when multiple components depend on it
For complex or large apps, consider Context API or Redux
Keep the parent responsible for the source of truth
Use descriptive prop names (
value,onChange) for clarity