AniUI Academy

Lifting State Up

What to do when two sibling components need to share and stay in sync with the same piece of state — move it to their nearest common ancestor and pass it back down.

7 min read

State is local to the component that declares it with useState — no other component can read or change it directly. That's a deliberate constraint, not a limitation to work around, but it raises an obvious question: what happens when two components need the same piece of data, and one of them changes it?

The problem

Imagine a search box and a results list that need to agree on the current query, but they're siblings — neither is inside the other:

function App() {
  return (
    <div>
      <SearchBox />   {/* has the query the user is typing */}
      <ResultsList /> {/* needs that same query, to filter results */}
    </div>
  );
}

If SearchBox holds the query in its own useState, ResultsList simply has no way to see it — it's private to SearchBox. Adding a useState for query inside ResultsList too doesn't help either; now there are two separate, disconnected values that happen to share a name.

The fix: move it up

Lifting state up means moving the shared state to the nearest component that is an ancestor of everyone who needs it — here, App — and passing it down to both children as props:

function App() {
  const [query, setQuery] = useState("");
 
  return (
    <div>
      <SearchBox query={query} onQueryChange={setQuery} />
      <ResultsList query={query} />
    </div>
  );
}
 
function SearchBox({ query, onQueryChange }) {
  return (
    <input value={query} onChange={(e) => onQueryChange(e.target.value)} />
  );
}
 
function ResultsList({ query }) {
  const results = ALL_ITEMS.filter((item) => item.name.includes(query));
  return <ul>{results.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}

SearchBox no longer owns the query — it receives the current value as a prop, and receives onQueryChange (which is really App's own setQuery, passed down under a more descriptive name) to ask for it to change. ResultsList just reads the value. Both children stay in sync because there's only one real piece of state, sitting in their common parent — the same discipline you've already seen with controlled inputs, just shared across more than one component now.

This is a pattern, not a special API

There's no liftStateUp() function — it's simply: identify the nearest shared ancestor, move the useState call there, and pass the value and a setter (or a function that wraps the setter) down as props to whoever needs either. It follows directly from the rule that data flows one way, down through props, and requests to change data flow back up through callback props.

The cost that's coming

This works cleanly for two or three components close together. It gets uncomfortable when the component that needs the data is nested several layers below the ancestor holding the state — every layer in between has to accept and forward props it doesn't actually use itself, purely so a descendant can eventually reach them. That specific pain — prop drilling — and when context is (and importantly, isn't) the right answer to it, is covered properly once this course reaches composition and context.

Try it yourself

A shared "selected color" swatch and a preview box, lifted to their common parent:

Try it yourself
Loading playground...

What to remember

  • State is private to the component that declares it — siblings can't read each other's state directly.
  • To share state between components, move it to their nearest common ancestor and pass the value (and a way to change it) down as props.
  • The child that changes shared state doesn't do so directly — it calls a callback prop that the parent supplies, which wraps the parent's own setter.
  • Lifting state up scales poorly across many layers of nesting — that specific problem, and its actual fix, comes later in this course.

Check yourself

3 questions · pass 3/3 to unlock useEffect Basics

up to 50
  1. 1.Two sibling components, SearchBox and ResultsList, both need access to the current search query. Where should that query live as state?

  2. 2.After lifting state up, how does the child that needs to change the value do so, given it no longer owns that state?

  3. 3.What is the main downside of lifting state up that this course will revisit later?

3 left to answer