Lifting State Up in React
React data flows in one direction: from parent to child. A component can pass data down to its children via props, but siblings cannot directly share data with each other.
When two components need the same piece of data, the solution is to move that state up to the nearest parent that contains both of them. The parent holds the state, then passes it down to each child as a prop. This is called lifting state up.
The problem
Here is a search feature built the wrong way. SearchInput has its own query state, and ResultsList has its own. They can never talk to each other:
// This does not work: each component has its own isolated state
function SearchInput() {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
function ResultsList() {
const [query, setQuery] = useState(''); // a different state, always empty
const items = ['Apple', 'Banana', 'Cherry', 'Apricot'];
const filtered = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
return (
<ul>
{filtered.map((item) => <li key={item}>{item}</li>)}
</ul>
);
}
SearchInput tracks what the user types, but ResultsList never sees it. The list always shows all items regardless of what is typed.
The solution: lift the state up
Move the query state to the parent. The parent passes it down to both children:
import { useState } from 'react';
function SearchInput({ query, onChange }) {
return (
<input
value={query}
onChange={(e) => onChange(e.target.value)}
placeholder="Search..."
/>
);
}
function ResultsList({ query }) {
const items = ['Apple', 'Banana', 'Cherry', 'Apricot'];
const filtered = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
return (
<ul>
{filtered.map((item) => <li key={item}>{item}</li>)}
</ul>
);
}
function App() {
const [query, setQuery] = useState('');
return (
<div>
<SearchInput query={query} onChange={setQuery} />
<ResultsList query={query} />
</div>
);
}
Now both components receive query from the same source. When the user types, SearchInput calls onChange, which updates state in App, which re-renders both components with the new value.
A complete example
Here is a shopping cart where items are shown in a list and the cart count shows at the top. The cart count needs to update when the user adds an item:
import { useState } from 'react';
const PRODUCTS = [
{ id: 1, name: 'Keyboard', price: 79 },
{ id: 2, name: 'Mouse', price: 49 },
{ id: 3, name: 'Monitor', price: 299 },
];
function CartSummary({ count }) {
return (
<header>
<h1>Shop</h1>
<p>Items in cart: {count}</p>
</header>
);
}
function ProductList({ onAddToCart }) {
return (
<ul>
{PRODUCTS.map((product) => (
<li key={product.id}>
{product.name} (${product.price})
<button onClick={() => onAddToCart(product)}>Add to cart</button>
</li>
))}
</ul>
);
}
function App() {
const [cartCount, setCartCount] = useState(0);
function handleAddToCart(product) {
setCartCount((prev) => prev + 1);
console.log(`Added ${product.name} to cart`);
}
return (
<div>
<CartSummary count={cartCount} />
<ProductList onAddToCart={handleAddToCart} />
</div>
);
}
export default App;
CartSummary and ProductList are siblings. Neither knows about the other. The parent (App) owns the cart count state, passes it to CartSummary for display, and passes the handler to ProductList so it can trigger updates.
Passing handler functions down
The standard pattern for a child communicating upward:
- The parent defines the state and a handler function.
- The parent passes the handler to the child as a prop.
- The child calls the handler when something happens.
function App() {
const [selected, setSelected] = useState(null);
function handleSelect(item) {
setSelected(item);
}
return (
<div>
{selected && <p>You selected: {selected}</p>}
<OptionList onSelect={handleSelect} />
</div>
);
}
function OptionList({ onSelect }) {
const options = ['React', 'Vue', 'Svelte'];
return (
<ul>
{options.map((option) => (
<li key={option}>
<button onClick={() => onSelect(option)}>{option}</button>
</li>
))}
</ul>
);
}
The child does not manage state. It just calls the function it was given and lets the parent decide what to do with the result.
When lifting state becomes unwieldy
Lifting state up works well when the components involved are close together in the tree. If you find yourself passing a value through three or four intermediate components that do not actually use it (just pass it along), that is called prop drilling and it gets tedious quickly.
The solution to prop drilling is the Context API , which lets you make state available to any component in the tree without passing it manually at every level.
Common mistakes
Lifting state too high
If you put everything in the top-level App component, every state change re-renders the whole app. Only lift state as high as it needs to go: to the nearest parent that contains all the components that need it.
Forgetting to pass the handler function
If a child component needs to update parent state, you must pass the handler function as a prop. Passing only the value is not enough.
// Wrong: ResultsList can read the query but cannot update it
<ResultsList query={query} />
// Correct: SearchInput needs both the value and the updater
<SearchInput query={query} onChange={setQuery} />
Calling the handler instead of passing it
// Wrong: handleSelect() runs immediately when the component renders
<OptionList onSelect={handleSelect()} />
// Correct: pass the function reference
<OptionList onSelect={handleSelect} />