When two components must agree, move their state up to the closest common parent. Learn how to pass data down and events up, where state should live, and what not to store.
In Part 3 a parent passed data down to a child with props. In Part 4 each component kept its own state. In Part 6 a parent passed a function down, and the child called it when something happened.
This part puts those three ideas together. Sometimes two components must always agree. Their state can’t stay in two places, so we move it up into their parent. React’s docs call this lifting state up.
We’ll see how to do it and how to decide where state should live. We’ll also see why you should store as little as you can.
Try this first
An accordion is a list of panels on a web page. You open a panel to read it. In this accordion, we want only one panel open at a time.
Read this code. Don’t press Run yet.
import { useState, type ReactNode } from 'react'
function Panel({ title, children }: { title: string; children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false)
return (
<section>
<h3>{title}</h3>
{isOpen ? <p>{children}</p> : <button onClick={() => setIsOpen(true)}>Show {title}</button>}
</section>
)
}
export default function Accordion() {
return (
<>
<Panel title="Rules">Each player gets five cards.</Panel>
<Panel title="Scores">The first player to reach ten points wins.</Panel>
</>
)
}
Each Panel shows its text when isOpen is true. If not, it shows a button. That is the ? : operator from Part 1.
Make a guess. You click “Show Rules”, then “Show Scores”. How many panels are open at the end?
Press Run and try it.
Both panels are open. We checked with Strict Mode on and off: 2 panels were open each time. That is not what we wanted.
Why the two panels can’t agree
Each Panel has its own isOpen. In Part 4 we saw that each place on the page gets its own state. So the “Rules” panel can’t see the “Scores” panel’s isOpen. It can’t close the other one either.
Two components that sit side by side under the same parent are called siblings. Siblings can’t pass data to each other directly. Data only flows down, from parent to child. Part 3 called this one-way data flow.
So the one fact both panels need, “which panel is open?”, must live somewhere both can get it from. The place is their parent, Accordion.
Lifting state up, in three steps
React’s docs do this in three steps. We’ll follow the same three steps, with our own short names.
Step 1: take the state out of the children
Panel stops keeping isOpen for itself. It gets isOpen as a prop instead:
import type { ReactNode } from 'react'
function Panel({ title, isOpen, children }: { title: string; isOpen: boolean; children: ReactNode }) {
return (
<section>
<h3>{title}</h3>
{isOpen ? <p>{children}</p> : <button>Show {title}</button>}
</section>
)
}
Now Panel can’t change isOpen. Its parent decides.
Step 2: pass fixed values from the closest common parent
The closest common parent is the nearest component that has both panels inside it. Here it’s Accordion. It passes fixed values for now, like isOpen={true} to the first panel and isOpen={false} to the second.
Step 3: put the state in the parent, and pass it down
Only one panel may be open. So the parent doesn’t need two true-or-false values. It needs one number: the index of the open panel. An index is a position in a list, counted from 0.
The child can’t change the parent’s state by itself. So the parent also passes down a function. The child calls it when its button is clicked.
import { useState, type ReactNode } from 'react'
type PanelProps = {
title: string
isOpen: boolean
onShow: () => void
children: ReactNode
}
function Panel({ title, isOpen, onShow, children }: PanelProps) {
return (
<section>
<h3>{title}</h3>
{isOpen ? <p>{children}</p> : <button onClick={onShow}>Show {title}</button>}
</section>
)
}
export default function Accordion() {
const [openIndex, setOpenIndex] = useState(0)
return (
<>
<Panel title="Rules" isOpen={openIndex === 0} onShow={() => setOpenIndex(0)}>
Each player gets five cards.
</Panel>
<Panel title="Scores" isOpen={openIndex === 1} onShow={() => setOpenIndex(1)}>
The first player to reach ten points wins.
</Panel>
</>
)
}
Run it. The “Rules” panel starts open, because openIndex starts at 0. Click “Show Scores”. The “Rules” panel closes, and the “Scores” panel opens.
Look at the two props each panel gets:
isOpen={openIndex === 0}is a value.openIndex === 0is true whenopenIndexis 0, and false if not. The value flows down. The panel only reads it.onShow={() => setOpenIndex(0)}is a function. When the panel calls it, a message goes back up to the parent.
Notice something else. With one number, “both panels open” can’t happen at all. The old code had two true-or-false values, so it could get into that state. React’s docs make the same point: lifting state up often changes what you store.
Watch it happen
Lifting state up: state moves to the parent, data flows down, and calls go back up. Press play, or step through it.
Here are the same six steps in words.
- At first, each
Panelkeeps its ownisOpen. Neither one can see the other. - We lift the state up, into
Accordion, their closest common parent. The two true-or-false values become one number,openIndex. It starts at 0, so “Rules” starts open. - Data flows down.
AccordionpassesisOpen={true}to “Rules” andisOpen={false}to “Scores”. - You click “Show Scores”. The button inside that panel calls
onShow. - The call goes up.
onShowis the functionAccordiongave it, so it runssetOpenIndex(1)inAccordion. Accordionrenders again, and so do both panels. Now “Rules” getsisOpen={false}, and “Scores” getsisOpen={true}.
We logged each render in step 6 with Strict Mode off. One click rendered Accordion once, then “Rules” once, then “Scores” once.
An everyday example
Think of two children who share one TV. If each child has their own remote control, they fight. One turns on a cartoon, and the other one turns on the news. So the parent keeps the only remote. When a child wants to change the channel, the child asks. The parent presses the button, and both children see the same show.
The exact version
The everyday example says the child “asks”. In React, asking means calling a function that came in as a prop. The child doesn’t know what that function does. Panel only knows “call onShow when my button is clicked”. The parent decides what it means.
Also, the parent doesn’t “press a button on the TV” directly. It sets its own state. React then renders the parent again, and the parent passes fresh props to every child.
Passing the set function, or a named handler
There are two common ways to hand the change down.
You can pass the set function itself. Here a search box and a list must agree on the search text. The text lives in App, their closest common parent.
import { useState } from 'react'
const fruits = ['Apple', 'Banana', 'Cherry', 'Lemon', 'Orange']
function SearchBar({ query, onQueryChange }: { query: string; onQueryChange: (text: string) => void }) {
return <input aria-label="Search" value={query} onChange={e => onQueryChange(e.target.value)} />
}
function FruitList({ query }: { query: string }) {
const shown = fruits.filter(fruit => fruit.toLowerCase().includes(query.toLowerCase()))
return (
<ul>
{shown.map(fruit => <li key={fruit}>{fruit}</li>)}
</ul>
)
}
export default function App() {
const [query, setQuery] = useState('')
return (
<div>
<SearchBar query={query} onQueryChange={setQuery} />
<FruitList query={query} />
</div>
)
}
Run it and type “an” in the box. The list shows only Banana and Orange.
You’ve seen filter and map in Part 5, and lists with a key in Part 8. Here filter keeps only the fruits that match, and map turns each one into an <li>. Two small things are new:
includeschecks if one piece of text is inside another.toLowerCase()makes both sides small letters, so “AN” finds “Banana” too.onQueryChange={setQuery}passes the set function as it is. No arrow function is needed. WhenSearchBarcallsonQueryChange('an'), that issetQuery('an').
The other way is a function you write, like onShow={() => setOpenIndex(0)} in the accordion. Use one when the child shouldn’t know how the parent stores things. Panel just says “show me”. It doesn’t know that the parent keeps a number.
Either way, name the prop on plus what happened, like onShow or onQueryChange. That is the habit from Part 6.
Controlled and uncontrolled components
React’s docs have two words for these two kinds of component.
- The first
PanelkeptisOpenin its own state. Its parent had no prop to set it with. A component like this is called uncontrolled. - The second
PanelgotisOpenas a prop. Its parent decided everything. The docs call a component controlled when its important information is “driven by props rather than its own local state”.
Each kind has a cost. An uncontrolled component is easier to use, because the parent passes less. But it’s hard to make two of them work together. A controlled component is easy to make work with others. But the parent must pass it everything it needs.
These are not strict rules. The docs say “controlled” and “uncontrolled” “aren’t strict technical terms”. Most components have some state of their own and some props.
You met the same word in Part 6, for a text box. An input whose value comes from state is a controlled input. That is the same idea, used for one <input> tag. Part 28 covers inputs in depth.
One source of truth
After lifting, the open panel is stored in exactly one place: openIndex in Accordion. Every panel reads it from there.
React’s docs call this having a “single source of truth”. It doesn’t mean all of your state lives in one component. It means each piece of state has one owner. Other components get it from that owner, as props.
When two components hold copies of the same fact, the copies can disagree. That is what happened in “Try this first”. With one owner, there is nothing to disagree with.
Where should a piece of state live?
React’s docs give a short method. Do this for each piece of state:
- Find every component that shows something based on that state.
- Find their closest common parent: the nearest component above them all.
- Put the state there. Sometimes a component higher up makes more sense. If no component fits, make a new one just to hold the state.
Try it on the search example. Which components use the search text?
SearchBarshows it in the box.FruitListuses it to pick the fruits.
Their closest common parent is App. So the search text lives in App.
Keep state as low as you can
Step 3 says to use the closest common parent. Why not just put all state at the very top, in App, to be safe?
Because when state changes, its component renders again. So do the components it creates in its own JSX. Part 2 showed that a child written inside a parent runs again when the parent runs.
Here is a note box. Only the box and its text use note. But the state lives in App, above a Sidebar that has nothing to do with it.
import { useState } from 'react'
function Sidebar() {
console.log('Sidebar renders')
return <p>Links: Home, Help</p>
}
export default function App() {
const [note, setNote] = useState('')
return (
<div>
<input aria-label="Note" value={note} onChange={e => setNote(e.target.value)} />
<p>You typed: {note}</p>
<Sidebar />
</div>
)
}
Sidebar renders
Sidebar renders
Sidebar renders
Sidebar renders
Sidebar renders
Sidebar renders
Press Run, then type “ab” in the box. The Console shows 6 lines.
Why 6? The playground runs in Strict Mode, so in development React calls each component twice each time it renders. Part 2 explained this. So the first render logs 2 lines. Each letter you type renders App again, and Sidebar with it: 2 more lines per letter.
Now move the state down, into a new NoteBox component that only holds the box:
import { useState } from 'react'
function Sidebar() {
console.log('Sidebar renders')
return <p>Links: Home, Help</p>
}
function NoteBox() {
const [note, setNote] = useState('')
return (
<div>
<input aria-label="Note" value={note} onChange={e => setNote(e.target.value)} />
<p>You typed: {note}</p>
</div>
)
}
export default function App() {
return (
<div>
<NoteBox />
<Sidebar />
</div>
)
}
Sidebar renders
Sidebar renders
Run it and type “ab” again. The Console shows only the 2 lines from the first render. Typing renders NoteBox and nothing else.
We counted the renders while typing three letters:
Where note lives |
Sidebar renders, Strict Mode off |
Sidebar renders, Strict Mode on |
|---|---|---|
In App (too high) |
3 | 6 |
In NoteBox (as low as it can be) |
0 | 0 |
These counts don’t include the first render. In a production build, Strict Mode doesn’t render twice, so you would get the first column.
A small Sidebar like this one is fast, so the extra renders don’t hurt here. But in a big app, state kept too high makes every key you type run many components. React’s docs say it plainly: “Prefer local state and don’t lift state up any further than necessary.”
So lift state up as far as you need to, and no further.
Don’t store what you can work out
Lifting state up removes copies between components. There is a second kind of copy, inside one component. It is a value you could work out from other state.
Temperature can be measured in Celsius, or in Fahrenheit. These are two different number scales for how hot something is. 100 in Celsius is the same heat as 212 in Fahrenheit. To turn Celsius into Fahrenheit, the rule is celsius * 9 / 5 + 32.
This example keeps both values in state:
import { useState } from 'react'
function CelsiusInput({ value, onChange }: { value: string; onChange: (text: string) => void }) {
return <input aria-label="Celsius" type="number" value={value} onChange={e => onChange(e.target.value)} />
}
export default function Thermometer() {
const [celsius, setCelsius] = useState('20')
const [fahrenheit, setFahrenheit] = useState(68)
function handleCelsiusChange(text: string) {
setCelsius(text)
setFahrenheit(Number(text) * 9 / 5 + 32)
}
return (
<div>
<CelsiusInput value={celsius} onChange={handleCelsiusChange} />
<p>Fahrenheit: {fahrenheit}</p>
<button onClick={() => setCelsius('100')}>Set to 100</button>
</div>
)
}
type="number" makes the box take numbers. Even so, the box gives your code text, so celsius holds text, like '25'. We keep it as text so that an empty box stays empty while you type. We tried keeping a number instead. Deleting the 20 put a 0 back in the box, and typing 2 and 5 then showed 025. Number(...) turns the text into a number for the math. An empty box counts as 0, because Number('') is 0.
Run it. Delete the 20 in the box and type 25. Fahrenheit shows 77, which is right. Now click “Set to 100”.
The box says 100, but Fahrenheit still says 77. The two values don’t match. We checked with Strict Mode on and off, and got the same result.
The bug is in the button. It sets celsius and forgets fahrenheit. Every place that changes one value must also change the other. One day, one place forgets.
The fix is to store only celsius. Fahrenheit is worked out from it, each time the component renders:
import { useState } from 'react'
function CelsiusInput({ value, onChange }: { value: string; onChange: (text: string) => void }) {
return <input aria-label="Celsius" type="number" value={value} onChange={e => onChange(e.target.value)} />
}
export default function Thermometer() {
const [celsius, setCelsius] = useState('20')
const fahrenheit = Number(celsius) * 9 / 5 + 32
return (
<div>
<CelsiusInput value={celsius} onChange={setCelsius} />
<p>Fahrenheit: {fahrenheit}</p>
<button onClick={() => setCelsius('100')}>Set to 100</button>
</div>
)
}
Run it, type 25 again, and click “Set to 100”. Fahrenheit now shows 212.
fahrenheit is a normal variable, not state. Every render works it out again from celsius, so it always matches celsius. The code is shorter too: handleCelsiusChange is gone.
A value you work out from other values like this is called a derived value. React’s docs give a rule. If you can work something out from props or state while rendering, don’t put it in state. The shown fruits in the search example are derived too. They come from fruits and query.
When the common parent is far away: prop drilling
Sometimes the closest common parent is far above the components that need the state. Then the value must pass down through many layers. The middle components don’t use it. They only pass it on.
Part 3 gave this a name: prop drilling. A few layers are fine. When there are many layers, React has a tool called context. Part 23 covers it.
Common mistakes
Copying a prop into state
import { useState } from 'react'
function NameTag({ name }: { name: string }) {
const [shownName] = useState(name)
return <p>Name tag: {shownName}</p>
}
export default function App() {
const [name, setName] = useState('Ana')
return (
<div>
<button onClick={() => setName('Ben')}>Change to Ben</button>
<p>Parent has: {name}</p>
<NameTag name={name} />
</div>
)
}
Run it and click the button. The parent says Ben, but the name tag still says Ana.
Part 4 showed why. React uses the value you pass to useState only on the first render. After that, the prop changes, but the copy in state does not. We say the copy has gone stale: it is old, and nothing updates it.
The fix: don’t copy it. Use the prop directly, <p>Name tag: {name}</p>. We checked: then the tag shows Ben.
Sometimes you really want only the first value, and later changes should be ignored. React’s docs suggest naming that prop initialName or defaultName. Then the name tells readers that later values are ignored.
Storing a value you can work out
import { useState } from 'react'
const fruits = ['Apple', 'Banana', 'Cherry', 'Lemon', 'Orange']
export function FruitSearch() {
const [query, setQuery] = useState('')
const [shown, setShown] = useState(fruits)
function handleChange(text: string) {
setQuery(text)
setShown(fruits.filter(fruit => fruit.toLowerCase().includes(text.toLowerCase())))
}
return (
<div>
<input aria-label="Search" value={query} onChange={e => handleChange(e.target.value)} />
<ul>
{shown.map(fruit => <li key={fruit}>{fruit}</li>)}
</ul>
</div>
)
}
shown can be worked out from query. Keeping it in state means every change must update both, like the temperature example. Remove the second useState. Work it out in the component body instead, the way FruitList does: const shown = fruits.filter(fruit => fruit.toLowerCase().includes(query.toLowerCase())).
Lifting state too high
Putting state at the top “to be safe” makes more components render on each change. You saw it with the note box above. Use the closest common parent of the components that really use the state. Keep the rest local.
Keeping state in both the child and the parent
If the parent owns openIndex, the child must not also keep its own isOpen. Two owners for one fact is the “Try this first” bug again. The child should read the prop, and call the parent’s function to ask for a change.
Practice
Press Edit on the examples above and try these.
- In the accordion with
openIndex, add a third panel called “Prizes”. Click its button. How many panels are open? - In the same accordion, add a “Hide” button inside each open panel, so that all panels can be closed. Hint: the state needs a value that means “no panel”.
- In the temperature example that stores only
celsius, add a second box for Fahrenheit that you can type in. Delete what is in it, then type 212. What does the Celsius box show? This one is harder. Try it, and watch the box you type in. - In the first note box example (state in
App), type three letters. How many lines does the Console show?
Answers
1. Add <Panel title="Prizes" isOpen={openIndex === 2} onShow={() => setOpenIndex(2)}>The winner gets a book.</Panel>. After you click “Show Prizes”, exactly 1 panel is open: “Prizes”.
2. Let the state hold a number or null: useState<number | null>(0). null means “no panel is open”. Add onHide: () => void to PanelProps, and take onHide out of the props. An open panel now shows two things, the text and a button. Two elements side by side need a Fragment, as in Part 1:
import type { ReactNode } from 'react'
type PanelProps = {
title: string
isOpen: boolean
onShow: () => void
onHide: () => void
children: ReactNode
}
function Panel({ title, isOpen, onShow, onHide, children }: PanelProps) {
return (
<section>
<h3>{title}</h3>
{isOpen ? (
<>
<p>{children}</p>
<button onClick={onHide}>Hide {title}</button>
</>
) : (
<button onClick={onShow}>Show {title}</button>
)}
</section>
)
}
In Accordion, pass onHide={() => setOpenIndex(null)} to each panel. Click “Hide Rules”, and no panel is open.
3. The Celsius box shows 100. But the simple way has a problem. Say you keep celsius as a number, and both boxes are worked out from it. Then React changes the box you type in, while you type. We tried it: after deleting the 68 and typing 212, the Fahrenheit box showed 0212. Deleting everything gives 0, and the 0 goes back into the box.
The fix is to store what the person typed, and which box they typed it in. Both temperatures are worked out from those two:
import { useState } from 'react'
function TemperatureInput({ label, value, onChange }: { label: string; value: string; onChange: (text: string) => void }) {
return <input aria-label={label} type="number" value={value} onChange={e => onChange(e.target.value)} />
}
export default function Thermometer() {
const [text, setText] = useState('20')
const [scale, setScale] = useState('c')
const celsius = scale === 'c' ? Number(text) : (Number(text) - 32) * 5 / 9
const fahrenheit = celsius * 9 / 5 + 32
return (
<div>
<TemperatureInput
label="Celsius"
value={scale === 'c' ? text : String(celsius)}
onChange={t => { setText(t); setScale('c') }}
/>
<TemperatureInput
label="Fahrenheit"
value={scale === 'f' ? text : String(fahrenheit)}
onChange={t => { setText(t); setScale('f') }}
/>
</div>
)
}
(f - 32) * 5 / 9 is the Celsius rule turned around. String(...) turns a number back into text. The box you type in shows exactly what you typed. The other box is worked out. While you type, the Celsius box shows long numbers. After the first “2”, it shows -16.666666666666668. That is correct. It is just not rounded.
4. 8 lines. The first render logs 2, because of Strict Mode. Each of the three letters logs 2 more.
Interview questions
Try to answer each one out loud before you open the answer.
What does “lifting state up” mean, and when do you do it?
You do it when two or more components must always show the same fact. You take the state out of those components. You put it in their closest common parent. The parent passes the value down as a prop. It also passes a function down, so a child can ask for a change. React’s Thinking in React page calls the part that goes back up “inverse data flow”. Inverse means the other way.
A strong answer adds that lifting often changes the shape of the state. Two true-or-false values became one openIndex, which also made “two panels open” impossible.
Should a child get the parent’s set function, or a handler the parent writes?
Both work. A set function is fine when the child sends the new value as it is, like onQueryChange={setQuery}. A handler is better when the child only reports what the user wants, like “show me”. Then the child doesn’t know how the parent stores the state.
That leaves the parent free to change the state’s shape. In Practice 2, openIndex changed from a number to “a number or null“. Panel still calls onShow() the same way. Only Accordion knows what the state looks like.
A strong answer adds the naming habit: onSomething for the prop, handleSomething for the parent’s function. It also says the child must never change the parent’s state any other way. Props are read-only.
What are controlled and uncontrolled components?
A component is uncontrolled when its important information is in its own state. The parent can’t set it through props. It can only make the child start over, by giving it a new key. It is controlled when that information comes from props, so the parent decides. Uncontrolled components are easier to use. Controlled ones are more flexible, because a parent can make several of them work together.
A strong answer says React’s docs don’t treat these as strict terms. Most components mix both. The same word is used for form inputs. An <input> with value and onChange is a controlled input.
What does “a single source of truth” mean in React?
Each piece of state has one owner, one component that holds it. Everyone else gets it from that owner as props. It doesn’t mean all state lives in one place. It means no fact is stored twice, so two copies can never disagree.
How do you decide where a piece of state should live?
Find every component that shows something based on it. Find their closest common parent. Put the state there. Sometimes a component above it makes more sense, or you make a new component just to hold the state.
A strong answer adds “as low as possible”. State placed too high makes more components render when it changes. In our test, typing three letters rendered a Sidebar 3 times when the state was in App. The Sidebar didn’t even use that state. With the state in a small NoteBox, it rendered 0 times. Those are Strict Mode off numbers.
Why shouldn’t you keep a value in state if you can work it out?
Then there are two values that must match, and every change must update both. One day a code path forgets, and the page shows wrong data. In our temperature example, a “Set to 100” button set Celsius to 100 but left Fahrenheit at 77. Work out the value while rendering instead: const fahrenheit = celsius * 9 / 5 + 32.
A strong answer adds one more case. If working it out is slow, you can remember the result with useMemo. Part 21 covers it. You still don’t put it in state.
What is wrong with copying a prop into state, like useState(props.value)?
React uses the starting value only on the first render. When the parent passes a new value later, the state keeps the old one. The child shows stale data. Use the prop directly. If you really want only the first value, name the prop initialValue, so readers know later changes are ignored. To start the state over when a prop changes, give the child a different key. Part 4 showed this.
What is prop drilling, and is it bad?
It’s passing a prop down through many layers of components, where the middle ones don’t use it. It happens when state lives far above the components that need it. A few layers are normal and easy to follow. When there are many layers, context lets a deep component read a value without every layer passing it. Part 23 covers context.
A strong answer says context isn’t the first fix. Sometimes you can move state lower, or pass components as children so fewer layers are in between.
Sources
- Sharing State Between Components, react.dev: the accordion, the three steps, “closest common parent”, controlled and uncontrolled components, and “single source of truth”.
- Thinking in React, react.dev: the method for deciding where state should live, and passing set functions down.
- Choosing the State Structure, react.dev: avoid values you can work out, don’t copy props into state, and the
initialanddefaultnames. - memo, react.dev: “Prefer local state and don’t lift state up any further than necessary.”
- Passing Data Deeply with Context, react.dev: prop drilling, and trying props or
childrenbefore context. - StrictMode, react.dev: why components render twice in development.
- Every count and result above (open panels, render counts, the stale name tag and the temperature example) comes from running React 19.3.0 for this post.