Lifting State Up – Sharing State Between Components
As React applications become bigger, components rarely work completely independently.
Imagine a product page:
App
│
┌────────┴────────┐
↓ ↓
ProductInfo CartSummary
│ │
└─────── ? ───────┘
Suppose ProductInfo knows that a product has been added to the cart.
But CartSummary also needs to know.
Where should the state live?
If both components maintain their own separate state, they can easily become inconsistent.
React provides a pattern for this:
Lifting State Up
We move the shared state to their closest common parent.
Parent
/ \
↓ ↓
Child A Child B
↑ ↑
shared state
Today we'll understand how this works and why it is one of the most important patterns in React.
🧠 What Does "Lifting State Up" Mean?
Suppose we have two components:
<TemperatureInput />
<TemperatureDisplay />
Both need to know the same temperature.
Instead of storing temperature independently in both components, we store it in their parent:
function App() {
const [temperature, setTemperature] = useState("");
return (
<>
<TemperatureInput
temperature={temperature}
setTemperature={setTemperature}
/>
<TemperatureDisplay
temperature={temperature}
/>
</>
);
}
The state has been lifted up from the child components into their common parent.
🤔 Why Do We Need This?
Imagine two components:
Component A
↓
count = 5
Component B
↓
count = 10
They are supposed to represent the same information.
That's a problem.
We want:
Parent State
↓
count = 5
↙ ↘
↓ ↓
Component A Component B
Now both components receive the same source of truth.
🎯 The Single Source of Truth
One of the most important ideas behind lifting state is:
There should be one authoritative place where a particular piece of shared state is stored.
For example:
const [count, setCount] = useState(0);
The parent owns the state.
Children receive what they need through props.
Parent
│
├── state
│
├── ↓ value
│
└── ↓ event handler
│
├── Child A
└── Child B
🔄 State Flows Down
React follows one-way data flow.
If the parent has:
const [count, setCount] = useState(0);
it can pass the value down:
<Child count={count} />
The child receives:
function Child({ count }) {
return <p>{count}</p>;
}
So:
Parent State
↓
Props
↓
Child
🔁 But How Does the Child Update Parent State?
This is where it gets interesting.
The child doesn't directly modify the parent's state.
Instead, the parent passes a function.
Parent:
function App() {
const [count, setCount] = useState(0);
function increaseCount() {
setCount(prev => prev + 1);
}
return (
<Child onIncrease={increaseCount} />
);
}
Child:
function Child({ onIncrease }) {
return (
<button onClick={onIncrease}>
Increase
</button>
);
}
The flow is:
Parent owns state
↓
Parent passes state
↓
Parent passes handler
↓
Child triggers handler
↓
Parent updates state
↓
Parent re-renders
↓
New state flows down
This is one of the most important React patterns.
🧩 Simple Example
Let's build a simple counter.
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<div>
<CounterDisplay count={count} />
<CounterButtons
onIncrease={() =>
setCount(prev => prev + 1)
}
/>
</div>
);
}
function CounterDisplay({ count }) {
return <h1>{count}</h1>;
}
function CounterButtons({ onIncrease }) {
return (
<button onClick={onIncrease}>
Increase
</button>
);
}
export default App;
Notice:
Parent owns state
const [count, setCount] = useState(0);
Display receives the value
<CounterDisplay count={count} />
Button receives the function
<CounterButtons onIncrease={...} />
The state stays in the parent.
🔥 Why Not Keep State Inside the Child?
We could do:
function CounterButtons() {
const [count, setCount] = useState(0);
}
But now CounterDisplay doesn't know about that state.
If multiple components need the same information, keeping the state inside one child makes sharing difficult.
So we move it upward.
📦 Example: Temperature Converter
This is a classic example of lifting state.
Suppose we want:
Celsius: [ 25 ]
Fahrenheit: 77
Both values represent the same temperature.
We don't need two independent sources of truth.
We can store Celsius in the parent:
import { useState } from "react";
function App() {
const [temperature, setTemperature] = useState("");
const fahrenheit =
temperature === ""
? ""
: temperature * 9 / 5 + 32;
return (
<div>
<TemperatureInput
value={temperature}
onChange={setTemperature}
/>
<h2>
Fahrenheit: {fahrenheit}
</h2>
</div>
);
}
The state is shared through the parent.
🧮 Derived Data
Notice something important:
We didn't create:
const [fahrenheit, setFahrenheit] = useState("");
Instead:
const fahrenheit =
temperature === ""
? ""
: temperature * 9 / 5 + 32;
Why?
Because Fahrenheit is derived from Celsius.
We already have the source data.
There's no need to create another state variable for something we can calculate.
This is an important state-management principle:
Don't store data in state if it can be calculated from existing state or props.
🛒 Real-World Example: Shopping Cart
Imagine an application with:
App
│
┌────────┴─────────┐
↓ ↓
ProductList CartSummary
ProductList needs to add products.
CartSummary needs to display the cart.
The cart state can live in App.
function App() {
const [cart, setCart] = useState([]);
function addToCart(product) {
setCart(prev => [...prev, product]);
}
return (
<>
<ProductList
onAddToCart={addToCart}
/>
<CartSummary
cart={cart}
/>
</>
);
}
Now:
App
│
cart state
/ \
↓ ↓
ProductList CartSummary
│ │
│ │
addToCart cart data
This is exactly how component communication starts to work in larger applications.
🔄 Child-to-Parent Communication
Technically, React's data flow is still one-way.
The child isn't directly sending state upward.
Instead:
Parent
↓
passes function
↓
Child
↓
calls function
↓
Parent updates state
This can feel like "child-to-parent communication," but the actual mechanism is:
Parent passes a callback to the child.
📞 Callback Props
A function passed through props is often called a callback prop.
Parent:
function handleDelete(id) {
console.log("Delete:", id);
}
<Task
onDelete={handleDelete}
/>
Child:
function Task({ onDelete }) {
return (
<button onClick={() => onDelete(10)}>
Delete
</button>
);
}
The child invokes:
onDelete(10);
The parent's function runs.
🧠 Passing Arguments Through Callbacks
This is extremely common.
Parent:
function handleSelect(id) {
console.log(id);
}
Pass:
<UserList onSelect={handleSelect} />
Child:
function UserList({ onSelect }) {
return (
<button onClick={() => onSelect(101)}>
Select User
</button>
);
}
The child doesn't need to know what the parent will do.
It simply calls the callback.
🧩 Lifting State in Forms
Yesterday we learned controlled forms.
Now let's combine that with lifting state.
Suppose we have:
App
│
├── SearchInput
│
└── SearchResults
Both need the search query.
Instead of storing the query separately:
SearchInput
↓
query state
SearchResults
↓
query state
we lift it:
App
│
query state
/ \
↓ ↓
SearchInput SearchResults
Parent:
function App() {
const [query, setQuery] = useState("");
return (
<>
<SearchInput
query={query}
onQueryChange={setQuery}
/>
<SearchResults
query={query}
/>
</>
);
}
Now both components always use the same query.
🔎 Search Example
function SearchInput({ query, onQueryChange }) {
return (
<input
value={query}
onChange={event =>
onQueryChange(event.target.value)
}
placeholder="Search..."
/>
);
}
Results:
function SearchResults({ query }) {
return (
<p>
Searching for: {query}
</p>
);
}
The parent owns the state.
🧠 Why This Pattern Is Powerful
Imagine a real application:
Search Bar
↓
Search State
↓
┌───────────────────────────────┐
│ │
↓ ↓
Search Results Result Count
All components can stay synchronized because they depend on the same state.
Without lifting state, you might end up with:
Search Bar → query A
Results → query B
Count → query C
Now synchronization becomes difficult.
🔄 Lifting State + Conditional Rendering
We can combine today's concept with Day 65.
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<>
<Navbar
isLoggedIn={isLoggedIn}
onLogout={() => setIsLoggedIn(false)}
/>
{isLoggedIn ? (
<Dashboard />
) : (
<Login
onLogin={() => setIsLoggedIn(true)}
/>
)}
</>
);
}
The parent owns authentication state.
Children receive the data/functions they need.
🧩 Lifting State + Lists
We can also combine this with Day 66.
function App() {
const [selectedId, setSelectedId] = useState(null);
const users = [
{ id: 1, name: "Saurabh" },
{ id: 2, name: "Rahul" },
{ id: 3, name: "Aman" }
];
return (
<UserList
users={users}
selectedId={selectedId}
onSelect={setSelectedId}
/>
);
}
Inside:
function UserList({
users,
selectedId,
onSelect
}) {
return (
<div>
{users.map(user => (
<button
key={user.id}
onClick={() => onSelect(user.id)}
>
{user.name}
</button>
))}
{selectedId && (
<p>
Selected User ID: {selectedId}
</p>
)}
</div>
);
}
Now we're combining:
State
+
Events
+
Props
+
Lists
+
Conditional Rendering
🚨 When Should You Lift State?
A good rule is:
Lift state when multiple components need access to the same changing data.
For example:
Component A needs state
Component B needs state
↓
Find common parent
↓
Move state there
↓
Pass value + handlers through props
⚠️ Don't Lift State Too Far
There is also an opposite mistake.
You don't need to move every piece of state to the highest possible component.
Suppose:
function PasswordInput() {
const [showPassword, setShowPassword] = useState(false);
}
If only PasswordInput needs this state, keep it there.
Don't move it all the way to App unnecessarily.
Good state placement is:
Keep state as close as possible to where it's needed, but lift it when siblings need to share it.
🧠 State Placement Rule
Think about it like this:
Only one component needs it?
↓
Keep state there.
Multiple siblings need it?
↓
Lift state to their common parent.
Many distant components need it?
↓
You may eventually need a broader state-management solution.
We'll encounter those patterns later.
🏗️ Example: Accordion
Imagine an FAQ:
What is React?
↓
Answer
What is State?
↓
Answer
What are Props?
↓
Answer
If we want only one item open at a time, the parent can own:
const [activeIndex, setActiveIndex] = useState(null);
Then each FAQ item receives:
isActive
onClick
This allows the parent to control which child is active.
🧠 Controlled Components Beyond Forms
The concept we've learned today is broader than forms.
A component can be "controlled" by its parent whenever the parent owns the relevant state and passes the current value plus callbacks.
For example:
<Tabs
activeTab={activeTab}
onTabChange={setActiveTab}
/>
or:
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
/>
or:
<Accordion
activeIndex={activeIndex}
onChange={setActiveIndex}
/>
The same pattern keeps appearing.
🏆 Best Practices
1. Find the common parent
When two components need the same state, identify their closest common parent.
2. Keep one source of truth
Don't maintain duplicate copies of the same state unnecessarily.
3. Pass data down through props
<Component value={value} />
4. Pass callbacks down for updates
<Component onChange={handleChange} />
5. Don't modify parent state directly
The child should call the callback.
6. Don't lift state unnecessarily
If only one component needs it, keep it local.
7. Avoid duplicate derived state
If something can be calculated from existing state, calculate it instead of storing another state variable.
🔥 Complete Example: Shared Counter
Let's put everything together.
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
function increase() {
setCount(prev => prev + 1);
}
function decrease() {
setCount(prev => prev - 1);
}
return (
<div>
<CounterDisplay count={count} />
<CounterControls
onIncrease={increase}
onDecrease={decrease}
/>
</div>
);
}
function CounterDisplay({ count }) {
return (
<h1>
Count: {count}
</h1>
);
}
function CounterControls({
onIncrease,
onDecrease
}) {
return (
<div>
<button onClick={onIncrease}>
+
</button>
<button onClick={onDecrease}>
-
</button>
</div>
);
}
export default App;
Notice how clean the responsibilities are.
App
Owns the state.
CounterDisplay
Displays the value.
CounterControls
Triggers updates.
App
│
count state
/ \
↓ ↓
Display Controls
↑ │
│ │
└──────────┘
callbacks
This is excellent component design.
💡 My Biggest Takeaway
Today's biggest takeaway is:
When multiple components need the same state, don't create duplicate state. Lift it to their closest common parent.
The pattern is:
Parent
│
┌───────┴────────┐
↓ ↓
Child A Child B
↑ ↑
└── callbacks ───┘
│
Parent State
And the fundamental React data flow remains:
State
↓
Props
↓
Child
↓
Callback
↓
Parent State Update
↓
Re-render
↓
Updated Props
Once you understand this, component communication becomes much easier to reason about.
