Must knowHooks · 04

Lifting State

Move state up so siblings can share it, and back down (colocate) when only one component needs it.

On this page · 7 sections
  1. In one minute
  2. The example: search + favorites
  3. How it works
  4. Common mistakes
  5. Interview Q&A
  6. Related
  7. Sources

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.

Lifted to ProductGrid
ProductGrid.tsx
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}
What you see
Ceramic Mug ♡ · Trail Backpack ♡ · Desk Lamp ♡ …
Search: (empty)
Rendered this step
ProductGrid–
Backpack card–
Other cards (5)–
  1. eventonToggle={() => setFavorites(toggle(p.id))} />Click ♡ on the Trail Backpack. The card calls onToggle, a function from ProductGrid.
  2. render phaseconst [favorites, setFavorites] = useState([])The state lives in ProductGrid, so the re-render starts there. Recorded: the grid and all six cards rendered.
  3. commit phase.sort(byFavorite(favorites))In return, the grid can sort favorites first: the Backpack moved to the top.
  4. commit phaseSearch “mug”, then clear it. The Backpack is filtered out and comes back still ♥, because the grid that holds the state never unmounted.

0 / 0
The same heart button with its state in two places (both recorded).

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

Common mistakes

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.

Sources

Primary source: react.dev/learn/sharing-state-between-components