Lifting state up

Give every piece of state exactly one owner, and let the children report instead of remember.

10of 20
≈ 11Min read
7Parts

The problem

Two components need the same value. A search box holds the query; a list needs it to filter. Neither can reach into the other — props only go down.

state in the wrong place
function Search() {
  const [query, setQuery] = React.useState("");   // trapped in here
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

function Panel() {
  return (
    <>
      <Search />
      <Results />   {/* has no way of knowing what was typed */}
    </>
  );
}

The move

Lift the state to the closest common parent of everything that needs it. The children become simple: one takes a value and reports changes, the other takes data and draws it. Neither remembers anything.

the state goes up; the value and a callback come down
function Panel() {
  const [query, setQuery] = React.useState("");
  const shown = doors.filter((d) => d.name.toLowerCase().includes(query.toLowerCase()));

  return (
    <>
      <Search value={query} onChange={setQuery} />
      <Results doors={shown} />
      <p>{shown.length} of {doors.length}</p>
    </>
  );
}

function Search({ value, onChange }) {
  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}

What the child sends up

A child that hands up a clean value can be reused anywhere. A child that hands up a DOM event ties every parent to the markup inside it.

send the meaning, not the event
// the parent now has to know about DOM events
<Search onChange={(e) => setQuery(e.target.value)} />

// better: the child translates, the parent gets a value
function Search({ value, onChange }) {
  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
<Search value={query} onChange={setQuery} />

Controlled and uncontrolled components

  • Uncontrolled is simpler and right when nothing outside cares.
  • Controlled is right the moment something outside does: two panels where only one may be open, a form that resets from a button elsewhere, a value that must persist.
  • Many components offer both — value if you pass it, an internal fallback if you do not. Write the uncontrolled one first, and lift only when you need to.
the same idea one level higher
// uncontrolled: it owns its open state, the parent cannot influence it
function Panel({ title, children }) {
  const [open, setOpen] = React.useState(false);
  …
}

// controlled: the parent owns it — now two panels can be exclusive
function Panel({ title, open, onToggle, children }) { … }

One owner, always

if you truly need a fresh start, say so with a key
// the parent resets the editor by changing its identity
<Editor key={selectedId} door={selected} />
Never copy a prop into state

const [name, setName] = useState(props.name) reads the prop once. When the parent changes it, the copy goes stale and the two disagree — the bug arrives as "it shows the old value until I reload".

How far up

  • Only as far as the closest parent that contains everything that needs it. Higher than that and every render repaints half the app.
  • If the common parent is five levels up and the middle three do nothing but pass props along, that is prop drilling — a sign to look at context, or at composition (children) which often removes the need entirely.
  • Server data — a list fetched from an API — is not really component state at all. It belongs in a cache like TanStack Query, which is a different lesson.

Composition removes the drilling

The element is created where the data already is. Everything in between just renders children, and never sees the prop.

pass the element, not the data
// Layout no longer needs to know about user at all
<Layout sidebar={<Profile user={user} />}>
  <Doors user={user} />
</Layout>

Practice