Frontend Master

JavaScript Curriculum

Lifting State Up
+60 XP

Lifting State Up

medium
~25 min·60 XP

The coffee shop menu has a SearchBar and a MenuGrid side by side. Typing in SearchBar must filter MenuGrid. They are siblings — neither can read the other's state. The solution: lift the search state to their shared parent.

Lifting State Up

When siblings need to share state, lift it to their nearest common ancestor. The parent becomes the single source of truth — it passes data down as props and callbacks down for children to trigger changes.

Before and After — The Problem and Fix

See what happens when state stays local vs when it is lifted:

lifting-state.jsx
// ❌ Before lifting — siblings can't share state
function SearchBar() {
  const [search, setSearch] = useState('') // local!
  return <input value={search} onChange={...} />
}
function MenuGrid() {
  // ❌ Can't access search from SearchBar!
  return <div>...</div>
}
function CartSidebar() {
  const [cart, setCart] = useState([]) // local!
  // ❌ MenuGrid can't add to this cart!
}
Problem
SearchBar has its own search state. MenuGrid can't see it. CartSidebar has its own cart. MenuGrid can't add to it.
→ Switch to "State lifted" to see the fix
ℹ️The lifting rule
If two components need the same state, find their closest common ancestor and put the state there. The ancestor owns the state. Both components receive it as props. Only the ancestor can change it — children request changes via callback props.

One-Way Data Flow — Visualized

Data travels down via props, events travel up via callbacks:

data-flow.jsx

Parent passes data to child via props. Child receives it as read-only. Any number of levels deep.

↓ props
App
owns: count, name
↓ props
Header
receives: name
↓ props
Menu
receives: count
CartBadge
receives: count
// App → Header → UserName
<App>
  <Header name={name} />  {/* prop flows down */}
</App>

function Header({ name }) {
  return <h1>Hi, {name}</h1>  // read-only
}
count (in App):0
💡State always lives as high as needed — but no higher
Only lift state as far as it needs to go. If only two siblings need it, their parent is the right place. If the whole app needs it, that is where Context or a state manager becomes useful (Lessons 15–16).

Siblings in Sync — Live Demo

SearchBar, CategoryFilter, and MenuGrid — all three connected via parent state:

sibling-communication.jsx
// ❌ Siblings can't talk to each other directly
function SearchBar() {
  const [search, setSearch] = useState('')
  // Can't send search to MenuGrid — it's a sibling!
}
function CategoryFilter() {
  const [category, setCategory] = useState('all')
  // Can't send category to MenuGrid either!
}
function MenuGrid() {
  // ❌ Has no access to search or category
  // Shows all items always
}
Simulated — siblings isolated ✗
SearchBar
search state lives here — siblings can't see it
CategoryFilter
category state lives here — siblings can't see it
MenuGrid
can't filter — has no access to search or category
⚠️Prop drilling starts here
Lifting state creates a pattern called prop drilling — passing props through multiple layers of components that don't use them, just to get data to a deeply nested child. When drilling goes more than 2–3 levels deep, consider Context (Lesson 15).

Your Challenge

Build App with search and category state. Pass search and onSearch to SearchBar. Pass category and onCategory to CategoryFilter. Pass search and category to MenuGrid which derives the visible items. All three siblings stay in sync through the parent.

Challenge

Build a filterable menu with three siblings: SearchBar (updates search), CategoryFilter (updates category), and MenuGrid (reads both). All three share state via a common parent App. Wire onSearch and onFilter callbacks upward.

lifting-stateshared-statesibling-communicationcommon-ancestorcallbacksone-way-data-flowcontrolled-componentsprops-drilling
Lifting State Up | Nexus Learn