React from the ground up

Lifting state up

Two components that need the same value: where the state should live, how a child asks for a change, and why the parent is usually the right owner.

Part 9 of 12

Intermediate15 min readBack to Courses

A value in two places at once

A selection panel and a detail view both care about which row is chosen. If each kept its own copy, the page could show two answers — a click in the list, and a detail view that never heard about it. The fix is older than React: one source of truth.

React's version of that rule is to lift the state to the closest common parent of everything that needs it. The parent owns the value with useState; the children are handed the value to show and a function to call when it should change.

Children report, parents decide

A child that must change something does not reach for it; it calls the function it was given: onSelect(student.id). The parent's handler runs, runs its setter, and the new value flows back down as props. Data down, events up — one loop, no exceptions.

That is also what makes a child reusable. List in this chapter knows nothing about students or details; it knows a row, a selected flag and a click. Point it at tunes or invoices tomorrow and it still works.

Controlled by the parent

A child with no state of its own, driven entirely by props, is called controlled — the same word as the controlled input from the forms chapter, and for the same reason. The parent decided; the child displays and reports.

The rule of thumb: keep state in the component that owns the thing it describes. A search box that only feeds its own list can own its query. A selection that two siblings share belongs above both.

The callback is the child’s whole interface

A child that reports upward needs nothing but a function prop. It does not need to know what the parent does with the value, whether it is stored, or where it goes next. onSelect(id) is the whole contract, and a child written against it can be dropped into a page with a different store and still work.

That is also why the callback should carry the fact rather than the instruction: onSelect(id), not setSelectedId(id). The second name leaks the parent’s state shape into a component that should not care.

One owner, and a prop to prove it

A useful check after lifting: the child should have no useState for the value it displays. If it still does, there are two copies, and the page can show either. Deleting the child’s state and reading the prop instead is the repair, and it usually removes a synchronising effect as well.

The value lives above both

const { useState } = React;

function Row({ label, active, onPick }) {
  return (
    <li>
      <button
        onClick={() => onPick()}
        className={active ? 'active' : ''}
        aria-pressed={active}
      >
        {label}
      </button>
    </li>
  );
}

function App() {
  const [picked, setPicked] = useState('tea');

  return (
    <main>
      <h1>Pick a drink</h1>
      <ul>
        <Row label="tea" active={picked === 'tea'} onPick={() => setPicked('tea')} />
        <Row label="coffee" active={picked === 'coffee'} onPick={() => setPicked('coffee')} />
      </ul>
      <p>You picked: {picked}</p>
    </main>
  );
}

The round trip, in order

A click in the child calls the callback the parent passed. That callback runs the parent's setter, which schedules a render of the parent. The parent renders both children again with new props, and the child that reported sees the new value in its next render. Data goes down; the report goes up; nothing is shared behind the scenes.

This is why the callback is a prop like any other, and why its name usually starts with on: onSelect, onChange, onRemove. It says what happened, not what to do, which leaves the decision with the owner.

Where to stop lifting

Only state that two components must agree on needs to rise. A search box that filters its own list can own the query; a selected row that drives a detail panel cannot. The closest common parent is the rule, not the root of the app — lifting too far turns a local concern into everyone's prop.

A useful test: if the value disappeared, which components would be wrong? Put it above all of them and no higher. The rest can stay where they are, which keeps the tree small and the changes local.

Three lifting mistakes: keeping a copy in the child and a copy in the parent, so the two can disagree; initialising child state from a prop once and expecting it to follow later changes — it will not; and passing the setter itself when the parent should transform the value first, which leaks the storage shape into the child.

Try these next

  • Add a "swap units" button that converts the stored value instead of keeping a second copy.
  • Move the unit labels into the child so the parent only owns the number.
  • Render a third child that shows a one-line summary, driven by the same state.

A list and a detail that agree

Run it and click between the students. Then move the selection into List as its own state, and watch the detail panel stop following — that is the bug lifting state prevents.

Live Preview
Console output will appear here...

Two inputs, one temperature

Run it and type in either field: the other follows because both read one value. Then move the Celsius state into the Celsius input and watch the two fields disagree — that is the bug lifting prevents.

Live Preview
Console output will appear here...

When two components disagree about the page, ask which one owns the value. The answer is almost always the parent above both — and moving it there removes the disagreement by construction.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Where should shared state live?