Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~24 min· topic 3 of 6

Topic 6.3

Lifting State Up

In one line

When two components must show or change the same data, move that state up to their closest common parent and pass it down as props, with callbacks so the children can ask for changes; a component driven by props this way is called controlled.

Think of it like this

Two children sharing one TV remote. If each child has their own remote with its own idea of the channel, they fight and the TV flips back and forth. The fix is to give the one remote to a parent sitting between them. Each child asks the parent ("cartoons, please"), and the parent changes the channel. Both children see the same channel, because there is only one.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Lifting state up
Moving state from children to their closest common parent so they can share it.
Closest common parent
The nearest component that has all the components needing the state somewhere inside it.
Callback prop
A function passed as a prop so a child can tell the parent something happened, like onShow.
Inverse data flow
A child calling a parent's function to ask for a state change. The new value then flows back down.
Controlled component
A component driven by props. The parent decides what it shows.
Uncontrolled component
A component that keeps its own state. The parent can't change what it shows.
Owner
The one component that declares a piece of state and changes it.

Step by step

01Two panels, two separate states

Each Panel keeps its own isActive. Clicking one doesn't affect the other, so both can be open at once. The design wants only one open: the panels need to share a single fact.

Panel.jsx (before)whole filejsx
function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false); // private to each panel
  return (
    <section>
      <h3>{title}</h3>
      {isActive ? <p>{children}</p> : <button onClick={() => setIsActive(true)}>Show</button>}
    </section>
  );
}

02Step 1: remove the state from the child

Delete useState from Panel and read isActive from props instead. Now the panel can't decide for itself; its parent will.

03Step 2: declare it once in the common parent

Accordion contains both panels, so it owns activeIndex. It passes each panel a boolean worked out from that one number.

Accordion.jsxwhole filejsx
function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>…</Panel>
      <Panel title="Recipe" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>…</Panel>
    </>
  );
}

04Step 3: pass a callback so the child can ask

The panel's button calls onShow. It doesn't know or care what the parent does with it. The parent sets activeIndex, renders, and both panels get new isActive props.

Panel.jsx (after)whole filejsx
function Panel({ title, children, isActive, onShow }) {
  return (
    <section>
      <h3>{title}</h3>
      {isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
    </section>
  );
}
Step 3: pass a callback so the child can askdiagram
Rendering diagram…

05The same pattern keeps two inputs in sync

A Celsius box and a Fahrenheit box must always agree. Store one fact in the parent (the temperature in Celsius) and derive the other box's value from it. Each input calls back up with its new text; the parent converts and stores Celsius.

Try it yourself

  1. 1

    Allow closing

    In the lifted accordion, let a panel close again: start activeIndex at null, and give the open panel a Hide button that calls a new onHide prop. Predict what activeIndex === 0 gives when activeIndex is null. (false: every panel is closed.)

  2. 2

    Add a third box to the converter

    Add a Kelvin input (K = C + 273.15). You need no new state: store { value, scale: "k" } when the user types there, and derive the other two from it.

  3. 3

    Control the dark-mode switch

    Lift the dark-mode switch's state into Settings and add a "Reset all" button that turns both switches off. Which prop do you pass instead of defaultOn?

Code & diagrams

Before: each panel owns its own state New tab

Click Show on both. They both open, because each panel has its own private `isActive` and neither knows about the other.

Preview

Press Run to see the page.

Console

Nothing logged yet.
After: the parent owns the state New tab

Now opening one panel closes the other. The panels are controlled: they show whatever the parent says, and ask for changes through `onShow`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Accordion rendered, activeIndex = 0
Two inputs, one source of truth New tab

Type in either box and the other follows. The parent keeps what you typed and which box you typed in; the other box is derived. Neither input has its own state.

Preview

Press Run to see the page.

Console

Nothing logged yet.
One component, controlled or uncontrolled New tab

Only the controlled Wi-Fi switch can be turned off by the Airplane mode button, because its value comes from the parent. The dark-mode switch can only be changed by clicking it.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to pass the callback

In the lifted accordion, change the panel's button to onClick={() => onShow()} and remove onShow={...} from the second <Panel>. Click its Show button.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: onShow is not a function

Break #2

Keep a copy in the child as well

In the lifted accordion, give Panel its own const [open, setOpen] = useState(isActive) and render open instead of isActive. Open the second panel.

terminal
$ Preview
── what you'll see ──
The first panel stays open as well: two panels open again.

Myth vs fact

Myth

Lifting state up copies it into the parent.

Fact

It moves it. The children stop having their own state and read the parent's value through props.

Myth

Put shared state at the top of the app to be safe.

Fact

Put it in the closest common parent. Higher placement renders more components on every change and spreads the logic further than needed.

Myth

Controlled components are better than uncontrolled ones.

Fact

Each is a trade-off: controlled gives the parent power, uncontrolled needs less wiring. Many good components support both.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    When a component supports both modes, choose the mode on the first render and never switch: going from on={undefined} to on={true} changes owners mid-life. React warns about exactly this for inputs ("A component is changing an uncontrolled input to be controlled", Topic 5.3). Libraries such as Radix and React Aria follow the value / defaultValue / onValueChange convention.

  • ▸

    Lifting state causes the parent and all its children to render on each change. If some children are expensive and unrelated, you can pass them as children from higher up (they won't re-render because their elements didn't change) or wrap them in memo (Topic 16.3).

  • ▸

    If you lift state many levels and pass it through components that don't use it, you've hit prop drilling (Topic 6.5 and 9.1). Context (Topic 9.2) and external stores (Phase 15) solve it; lifting is still the first tool to reach for.

  • ▸

    With onChange={setWifi} the setter itself is the callback. That's fine: setter functions from useState are stable (the same function on every render), so passing them down doesn't break memo.

Remember this

  1. 1

    State is private to the component that declares it (Topic 4.2). If two sibling components each keep their own copy of something they should share, such as "which panel is open" or "the temperature", they can't agree, because neither can see the other's state. Lifting state up means removing the state from the children and declaring it once in their closest common parent: the nearest component that has both of them inside it.

  2. 2

    Lifting is three steps. 1. Remove the state from the children. 2. Pass the value down from the parent as a prop (isActive={activeIndex === 0}). 3. Pass a function down as well, so a child can ask the parent to change it (onShow={() => setActiveIndex(0)}). Values go down, requests go up. That second half is sometimes called inverse data flow: the child calls a function, the parent updates its state, and the new value flows back down.

  3. 3

    Why the closest common parent and not the very top of the app? Because every state change renders the component that owns it and everything below. State placed too high makes unrelated parts of the page render, and it makes the code harder to follow. Put it as low as possible, but high enough that everyone who needs it is below it (Topic 6.6).

  4. 4

    A component whose important information comes from its own state is called uncontrolled: the parent can't change what it shows. A component whose important information comes from props is controlled: the parent decides everything. The same idea you met with inputs (Topic 5.1 and 5.3) applies to your own components. Lifting state up turns uncontrolled children into controlled ones.

  5. 5

    Neither is always better. Uncontrolled components are easy to drop in (less to wire up); controlled components are flexible (the parent can coordinate, reset or save them). Good reusable components often support both: if the parent passes a value prop (open), use it; otherwise keep internal state, started from a defaultOpen prop. That's exactly how <input value> versus <input defaultValue> works.

  6. 6

    Each piece of state should have one owner: one component that declares it and is the only place that changes it. That is the single source of truth principle. Lifting state doesn't copy it; it moves it. If you find the same state declared in a child and a parent, one of them is a stale copy (Topic 6.2).

Explain it without notes

01

What does "lifting state up" mean, and when do you need it?

02

Describe the three steps to lift state from two sibling components.

03

What is the difference between a controlled and an uncontrolled component? Give an example of each.

04

How can one component support both controlled and uncontrolled use?

Practice

01

Two LikeCounter components each keep their own count. Make them share one total in the parent, so clicking either adds to the same number shown by both.

02

Make a Tabs component that is controlled: it receives tabs, selected and onSelect, and renders one button per tab with aria-selected. Use it from a parent that owns the selected tab.

Trade-offs

  • ↔

    Controlled components are flexible but need more wiring in every parent; uncontrolled ones are simpler to use but can't be coordinated or reset from outside (except with a key).

  • ↔

    Lifting state higher shares it with more components but makes more components render on each change; keep it at the closest common parent.

Done when you can

  • I can recognise when two components need shared state.

  • I can lift state to the closest common parent in three steps.

  • I pass values down and callbacks up.

  • I can explain controlled versus uncontrolled components.

  • I can build a component that supports both value and defaultValue styles.