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.
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.
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.
function Panel({ title, children, isActive, onShow }) {
return (
<section>
<h3>{title}</h3>
{isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
</section>
);
}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
Allow closing
In the lifted accordion, let a panel close again: start
activeIndexatnull, and give the open panel a Hide button that calls a newonHideprop. Predict whatactiveIndex === 0gives whenactiveIndexisnull. (false: every panel is closed.) - 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
Control the dark-mode switch
Lift the dark-mode switch's state into
Settingsand add a "Reset all" button that turns both switches off. Which prop do you pass instead ofdefaultOn?
Code & diagrams
Click Show on both. They both open, because each panel has its own private `isActive` and neither knows about the other.
Preview
Console
Nothing logged yet.Now opening one panel closes the other. The panels are controlled: they show whatever the parent says, and ask for changes through `onShow`.
Preview
Console
Nothing logged yet.Expected console output
Accordion rendered, activeIndex = 0Type 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
Console
Nothing logged yet.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
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.
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.
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}toon={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 thevalue/defaultValue/onValueChangeconvention. - ▸
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
childrenfrom higher up (they won't re-render because their elements didn't change) or wrap them inmemo(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 fromuseStateare stable (the same function on every render), so passing them down doesn't breakmemo.
Remember this
- 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
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
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
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
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 adefaultOpenprop. That's exactly how<input value>versus<input defaultValue>works. - 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
What does "lifting state up" mean, and when do you need it?
Describe the three steps to lift state from two sibling components.
What is the difference between a controlled and an uncontrolled component? Give an example of each.
How can one component support both controlled and uncontrolled use?
Practice
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.
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
valueanddefaultValuestyles.