skip to content

In React, two sibling components need the same value — one displays it and the other edits it. Where does that state go, and what does each sibling receive instead?

level: juniorimportance: must knowfreq 78%

answer

  1. siblings cannot talk sideways
  2. nearest common ancestor
  3. value down, events up
  4. child gives up its own copy
  5. one useState behind both renders

basics

~20 s

Move the state into the siblings' nearest common ancestor. That parent owns it with useState and passes the current value down to both children, plus a callback that lets the editing child ask for a change.

solid answer

~50 s

State that two siblings both depend on cannot live in either of them — a component can only pass data to what it renders, so siblings have no way to reach each other. I lift the state to their nearest common ancestor: that parent holds it with `useState` and passes two things down — the current value to whichever children display it, and a callback such as `onChange` to the child that needs to change it. The editing child stops keeping its own copy and becomes controlled for that value: it renders what it is handed and reports intent upward. That gives a single source of truth, so the two children can never disagree, and it keeps React's flow one-directional — data down through props, events up through callbacks. I lift only as far as the closest ancestor both children sit under.

code

javascript · 25 lines
javascript
import { useState } from 'react';

function SearchBox({ query, onQueryChange }) {
  return (
    <input
      value={query}
      onChange={(e) => onQueryChange(e.target.value)}
      placeholder="Search"
    />
  );
}

function ResultCount({ query }) {
  return <p>{query ? `Searching for "${query}"` : 'Showing everything'}</p>;
}

export default function Filters() {
  const [query, setQuery] = useState('');
  return (
    <>
      <SearchBox query={query} onQueryChange={setQuery} />
      <ResultCount query={query} />
    </>
  );
}

go deeper

for a junior

Be ready to name the move in one sentence: shared state goes to the nearest common parent, which passes the value down and a change callback back up. Say plainly that siblings cannot read each other's state.

for a middle

Explain the mechanics rather than the slogan: the child stops holding its own copy and becomes controlled for that value, so exactly one useState sits behind both renders and they update in the same commit. Be able to write the wiring.

for a senior

Show judgment about the callback contract — a narrow intent callback instead of a raw setter — and about how far to lift. Point out that repeated lifting of the same value is usually telling you the component boundary is drawn in the wrong place.

for a principal

Own the tradeoff that lifting is coupling. Talk about where ownership lines are drawn so shared state does not creep toward the root, and about keeping a consistent value-plus-onChange convention across the codebase so ownership can move later without rewriting call sites.

## Why the state cannot stay where it is React's data flow is one-directional. A component can hand values to the components it renders, but it has no handle on its parent and no handle on its siblings. If an `Editor` keeps the value in its own `useState` and a `Display` rendered beside it needs the same value, there is no supported way for `Display` to read it — and if both call `useState` independently, they hold two unrelated values that drift apart the instant either one updates. "Lifting state up" is the fix: delete the `useState` from the child and move it into a component that renders both of them. ## The rule: nearest common ancestor The home for a piece of state is the **lowest component that renders every component which reads or writes it**. For one consumer that is the consumer itself (colocation). For two siblings it is their immediate parent. For two components in different branches it is wherever those branches meet. "Nearest" matters as much as "common". Any component above that point is now carrying state it does not use, and every component between it and the consumers has to pass the props through. ## Data down, intent up After lifting, the parent owns the value and hands each child exactly what it needs: ```jsx function Filters() { const [query, setQuery] = useState(''); return ( <> <SearchBox query={query} onQueryChange={setQuery} /> <ResultCount query={query} /> </> ); } ``` `SearchBox` no longer decides anything. It renders `props.query` and, when the user types, calls `props.onQueryChange(next)`. It has become *controlled* for that value: the parent is the single source of truth, and the child is a pure function of its props plus a reporter of user intent. `ResultCount` only reads, so it gets the value alone. This is why React interviews describe the pattern as "data flows down, events flow up". The value makes one trip down the tree; the request to change it makes one trip up; the parent re-renders and both children see the new value in the same commit, so they cannot be out of sync. ## The callback's shape is a design decision Passing the setter itself (`onQueryChange={setQuery}`) is fine when the child genuinely owns the whole value and the parent stores it as-is. Prefer a narrow, named callback when the parent must do something with the change — validate it, normalise it, update two fields at once, or store the value in a different shape: ```jsx <RowList rows={rows} onSelect={(id) => setSelection({ id, at: Date.now() })} /> ``` The narrow callback keeps the parent's state shape private. If the child receives `setState` directly, the child's contract silently depends on how the parent stores things, and refactoring the parent's state breaks the child. ## Do not keep a second copy in the child The classic mistake after lifting is leaving `const [value, setValue] = useState(props.value)` behind in the child "so it stays fast" or "so it works standalone". Now there are two sources of truth: the parent's value and the child's snapshot of it, taken on the first render. Later parent updates do not reach the child, and the UI shows a stale value that no amount of prop passing fixes. If the child needs the value, it reads the prop. ## Lift the minimum, not everything Lifting is coupling: the parent now knows about something two of its children share, and both children now depend on the parent to supply it. So lift the *smallest* thing that must be shared, and only that. A modal's `isOpen` shared by a trigger and the modal itself gets lifted; the modal's internal hover state does not. Purely local state — whether a tooltip is showing, whether a row is expanded and nobody else cares — stays where it is. When the same value gets lifted twice more and starts travelling through several layers that ignore it, that is a signal to reconsider the structure, not automatically a signal to reach for a global store. ## What interviewers are listening for Three things. First, that you say *nearest* common ancestor rather than "put it in App". Second, that you describe both directions of the wiring — value down, callback up — instead of only the value. Third, that you recognise the single-source-of-truth consequence: the child gives up its own copy, and that is the point of the exercise, not a side effect.

  • Should the child receive the state setter itself, or a narrower callback?
    Pass the setter only when the child owns the whole value and the parent stores it unchanged. Otherwise pass a named intent callback — `onSelect(id)`, `onIncrement()` — so the parent can validate, normalise, or update several fields at once. A narrow callback keeps the parent's state shape private; handing over `setState` makes the child depend on that shape and turns every parent refactor into a child change.
  • After lifting, is the child allowed to keep any state of its own?
    Yes — just not a copy of the lifted value. State nobody else observes stays local: whether a tooltip is visible, whether a row is expanded, an in-progress draft that only becomes shared on submit. Keeping a second copy of the lifted value is the problem, because it is seeded once and then silently diverges from the parent's.
  • How do you decide you have lifted far enough?
    Stop at the lowest component that renders every reader and writer. If you have to go higher, that is the correct home; if you went higher than necessary, the extra ancestors carry state they never use and the components in between pass props they ignore. "Put it in App" is over-lifting unless App genuinely is the nearest common ancestor.

saying these in an interview costs you the question

  • Says one sibling can read the other's state directly
  • Reaches for Context or Redux to share between two siblings
  • Keeps a copy in the child and syncs it with an effect
  • Thinks lifting always means moving state to App
  • Passes only the value down and forgets the callback

context