In one minute
Data in React flows down through props, so two sibling components can’t share state directly. Lifting state moves the useState up to their closest common parent, which passes the value (and a way to change it) down to both. The opposite skill is colocation: once only one component needs a piece of state, move it back down into that component. Lower state means less code passing props around and fewer components re-rendering when it changes, but the state now lives only as long as that component does.
You’ll be able to: decide where a piece of state belongs, and predict who re-renders when it changes.
1function ProductGrid({ query }) {2 const [favorites, setFavorites] = useState([])3 const found = searchProducts(query)4 .sort(byFavorite(favorites))5 return found.map(p => (6 <Card key={p.id} product={p}7 isFavorite={favorites.includes(p.id)}8 onToggle={() => setFavorites(toggle(p.id))} />9 ))10}- event
onToggle={() => setFavorites(toggle(p.id))} />Click ♡ on the Trail Backpack. The card callsonToggle, a function fromProductGrid. - render phase
const [favorites, setFavorites] = useState([])The state lives inProductGrid, so the re-render starts there. Recorded: the grid and all six cards rendered. - commit phase
.sort(byFavorite(favorites))In return, the grid can sort favorites first: the Backpack moved to the top. - commit phaseSearch “mug”, then clear it. The Backpack is filtered out and comes back still ♥, because the grid that holds the state never unmounted.
1function Card({ product }) {2 const [isFavorite, setIsFavorite] = useState(false)3 return (4 <li>5 {product.name}6 <button onClick={() => setIsFavorite(!isFavorite)}>7 {isFavorite ? '♥' : '♡'}8 </button>9 </li>10 )11}- event
<button onClick={() => setIsFavorite(!isFavorite)}>Click ♡ on the Trail Backpack. The card owns its state. - render phase
const [isFavorite, setIsFavorite] = useState(false)The re-render starts at that card. Recorded: one line,Card p3 renders. Less work and simpler props, but nothing above can sort by favorites. - commit phaseSearch “mug”: the Backpack’s card unmounts, and its state with it.
- commit phaseClear the search: the card comes back as a new instance, ♡ again (recorded). Colocated state lives only as long as its component.
The example: search + favorites
The search box and the product grid are siblings, so the query lives in their parent:
function App({ Grid }: { Grid: (props: { query: string }) => React.ReactNode }) {
log('App renders')
const [query, setQuery] = useState('')
return (
<>
<Search query={query} setQuery={setQuery} />
<Grid query={query} />
</>
)
}
Each product card has a ♡ button. Where should “is this a favorite” live?
Lifted into the grid (so it can sort)
function GridLifted({ query }: { query: string }) {
log(' ProductGrid renders')
const [favorites, setFavorites] = useState<string[]>([])
const found = searchProducts(query).sort((a, b) => Number(favorites.includes(b.id)) - Number(favorites.includes(a.id)))
return (
<ul>
{found.map((p) => (
<CardLifted
key={p.id}
product={p}
isFavorite={favorites.includes(p.id)}
onToggle={() => setFavorites((f) => (f.includes(p.id) ? f.filter((id) => id !== p.id) : [...f, p.id]))}
/>
))}
</ul>
)
}
function CardLifted({ product, isFavorite, onToggle }: { product: Product; isFavorite: boolean; onToggle: () => void }) {
log(` Card ${product.id} renders`)
return (
<li data-id={product.id}>
{product.name}
<button onClick={onToggle}>{isFavorite ? '♥' : '♡'}</button>
</li>
)
}
Clicking ♡ on the Trail Backpack, recorded:
ProductGrid renders
Card p3 renders
Card p1 renders
Card p2 renders
Card p4 renders
Card p5 renders
Card p6 renders
[
"Trail Backpack♥",
"Ceramic Mug♡",
"Wireless Headphones♡",
"Desk Lamp♡",
"Bluetooth Speaker♡",
"Notebook Set♡"
]
The grid and all six cards re-rendered, but the grid could sort favorites first. Searching “mug” (which hides the Backpack) and clearing the search again:
Trail Backpack♥
Still a favorite: the grid never unmounted.
Colocated in each card (no sorting)
function GridColocated({ query }: { query: string }) {
log(' ProductGrid renders')
return (
<ul>
{searchProducts(query).map((p) => (
<CardColocated key={p.id} product={p} />
))}
</ul>
)
}
function CardColocated({ product }: { product: Product }) {
log(` Card ${product.id} renders`)
const [isFavorite, setIsFavorite] = useState(false)
return (
<li data-id={product.id}>
{product.name}
<button onClick={() => setIsFavorite(!isFavorite)}>{isFavorite ? '♥' : '♡'}</button>
</li>
)
}
Card p3 renders
One card re-rendered. After hiding and showing it with the search:
Trail Backpack♡
The heart reset: the card unmounted when filtered out, and its state went with it.
How it works
- Re-renders start where the state lives.
setFavoritesin the grid re-renders the grid and its children;setIsFavoritein a card re-renders that card. Lifting state up widens that circle; colocating narrows it. - Lift to the lowest common parent, not higher. The query lives in
Appbecause bothSearchandProductGridneed it; favorites didn’t need to go toApp. - Effects follow their state. An effect that syncs a piece of state (like the
popstatelistener for the query) moves with it. - Colocation trades lifetime for simplicity. If a value must survive its component being unmounted (filtered out, a tab switched), keep it higher, or outside React (URL, storage, server).
Common mistakes
- Lifting everything to the top “just in case”, which makes every change re-render the whole page.
- Leaving state lifted after the feature that needed it is gone.
- Duplicating state in parent and child instead of passing it down.
Interview Q&A
What does lifting state mean, and when do you need it?
Moving state to the closest common parent of the components that need it, then passing it down as props (plus a setter or callback). You need it when siblings must read or change the same value.
Why did favorites have to live in the grid to sort by them?
Sorting compares every card’s favorite status, and only a component that sees all of them can do that. A card can’t sort a list it doesn’t own.
What is state colocation, and why does it help?
Keeping state in the lowest component that uses it. Fewer props, simpler parents, and fewer re-renders: recorded, a lifted favorite re-rendered the grid and six cards, a colocated one re-rendered a single card.
What's the downside of colocating?
State disappears when its component unmounts. Recorded: the colocated Backpack lost its ♥ after being filtered out and back; the lifted one kept it.
Related
- Managing UI State: derived state and controlled inputs.
- React Re-rendering: why children re-render with their parent.
Sources
- react.dev: Sharing state between components, Preserving and resetting state
- Kent C. Dodds, State Colocation will make your React app faster
- Topic order inspired by Kent C. Dodds’ EpicReact React Hooks workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/learn/sharing-state-between-components