Sharing state between components
Lift state up to the closest common parent so two components stay in sync. Controlled and uncontrolled components, with live examples and an out-of-sync test.
- 01Lift state upMove state out of two components into their closest common parent, and pass it back down as props, so they can't disagree.
- 02Tell controlled from uncontrolledSay whether a component or an input is driven by its own state or by props, and what each choice lets the parent do.
- 03Pass handlers downLet a child change its parent's state by calling a function the parent passed it, like
onOpenoronChange.
Two answers that should take turns
A product page for green tea has a short list of questions. Click a question, and its answer opens. The shop wants one answer open at a time, so the page stays short. Here is a first try, built the way the State lesson taught: each question keeps its own memory. Click both questions in the result:
1import { useState } from "react";2 3function FaqItem({ question, children }) {4 const [isOpen, setIsOpen] = useState(false);5 return (6 <section>7 <button onClick={() => setIsOpen(true)}>{question}</button>8 {isOpen && <p>{children}</p>}9 </section>10 );11}12 13export default function App() {14 return (15 <>16 <h2>Green tea: questions</h2>17 <FaqItem question="When will it arrive?">We send every order within 2 days.</FaqItem>18 <FaqItem question="Can I return it?">Yes, within 30 days, for a full refund.</FaqItem>19 </>20 );21}Line 4 gives each FaqItem its own isOpen, starting at false. Line 7 is the question, a button that sets isOpen to true. Line 8 shows the answer only while isOpen is true. App renders two FaqItems on lines 17 and 18.
Both answers open, and nothing can close the first when you open the second. Each FaqItem has its own state, and neither can see the other's. To open one and close the other, something has to know about both. That something is their parent, App:
1import { useState } from "react";2 3function FaqItem({ question, children, isOpen, onOpen }) {4 return (5 <section>6 <button onClick={onOpen}>{question}</button>7 {isOpen && <p>{children}</p>}8 </section>9 );10}11 12export default function App() {13 const [openId, setOpenId] = useState("arrive");14 return (15 <>16 <h2>Green tea: questions</h2>17 <FaqItem18 question="When will it arrive?"19 isOpen={openId === "arrive"}20 onOpen={() => setOpenId("arrive")}21 >22 We send every order within 2 days.23 </FaqItem>24 <FaqItem25 question="Can I return it?"26 isOpen={openId === "return"}27 onOpen={() => setOpenId("return")}28 >29 Yes, within 30 days, for a full refund.30 </FaqItem>31 </>32 );33}Now FaqItem has no state. Line 3 takes two new props: isOpen, whether to show the answer, and onOpen, a function to call when the question is clicked. Line 13 gives App one piece of state, openId: the id of the open answer, "arrive" at first. Lines 19 and 26 work out each item's isOpen from it, and lines 20 and 27 pass functions that set it. Click Can I return it?: its answer opens and the first one closes. Both items read the same openId, so only one can match.
This move has a name: lifting state up. You take state out of the components that need to agree, put it in their closest common parent, and pass it back down as props.
FaqItem keeps its own isOpen, and nothing stops both from being true. Right: the state moved up into App as one openId. Each FaqItem gets isOpen as a prop, so only one can be open.In a family where everyone keeps their own diary, two people can book the car for the same evening, and nobody finds out until Friday. Put one calendar on the fridge instead. Everyone reads it, and everyone writes their booking there, so it can't say two things at once.
- In real life: Each person writes plans in their own diary
- In React: Each FaqItem keeps its own isOpen
- In real life: Two diaries both book the car for Friday
- In React: Both answers think they're the open one
- In real life: One calendar on the fridge, where everyone passes
- In React: One openId in App, the closest common parent
- In real life: To book the car, you write on the fridge calendar
- In React: To open, a FaqItem calls onOpen, which sets App's state
To share state between components, lift it up. Remove it from the components that need it, keep it in their closest common parent, and pass it down as props, together with functions the children call to change it.
Lifting state up in three steps
You can lift any piece of state in the same three steps. Here they are for the FAQ.
Step 1: remove the state from the children. Delete the useState line from FaqItem, and read isOpen from its props instead. The child no longer decides; its parent will.
Step 2: pass fixed values from the parent. Before adding state, check that the children follow the prop. Here App hard-codes one open and one closed:
1function FaqItem({ question, children, isOpen }) {2 return (3 <section>4 <button>{question}</button>5 {isOpen && <p>{children}</p>}6 </section>7 );8}9 10export default function App() {11 return (12 <>13 <h2>Green tea: questions</h2>14 <FaqItem question="When will it arrive?" isOpen={true}>15 We send every order within 2 days.16 </FaqItem>17 <FaqItem question="Can I return it?" isOpen={false}>18 Yes, within 30 days, for a full refund.19 </FaqItem>20 </>21 );22}Line 1 is step 1: isOpen is a prop now, read by line 5. Lines 14 and 17 are step 2: the first item gets isOpen={true} and the second isOpen={false}. The result shows the first answer only. The buttons do nothing yet; line 4 has no onClick. Press Edit & run and swap the two values: the second answer shows instead. The children follow whatever App says.
Step 3: add state to the parent, and pass it down with a way to change it. That's the version in the overview. App keeps openId (line 13), passes each item isOpen worked out from it, and passes onOpen. When you click a question, line 6 calls onOpen. That function was made inside App on line 20 or 27, so it can call App's setOpenId. (A function can use the variables of the place it was written: that's a closure.) App renders again with the new openId, and both items get new props.
You could try to keep both isOpens and make each item close the other. But siblings can't reach each other's state, and two copies that must agree will, sooner or later, disagree. One copy, in the parent, can't.
Two price inputs that disagree
A tea shop lets you set a top price with a slider, and also with a number box beside it, for people who'd rather type. They're two inputs for the same filter, so they must always show the same number. Here they each keep their own:
1import { useState } from "react";2 3function PriceInput({ label, type }) {4 const [maxPrice, setMaxPrice] = useState(10);5 return (6 <p>7 <label>8 {label}{" "}9 <input10 type={type}11 min="1"12 max="10"13 value={maxPrice}14 onChange={(event) => setMaxPrice(Number(event.target.value))}15 />16 </label>{" "}17 Up to ${maxPrice}18 </p>19 );20}21 22export default function App() {23 return (24 <>25 <PriceInput label="Slider" type="range" />26 <PriceInput label="Box" type="number" />27 </>28 );29}Line 4 gives each PriceInput its own maxPrice, starting at 10. Lines 13 and 14 connect the input to it: the input shows maxPrice, and each change sets it. (Number(…) turns the input's text into a number.) Line 17 prints the price. App uses the same component twice, on lines 25 and 26, once as a slider and once as a box.
Drag the slider. Its line changes, and the box still says Up to $10. Type 3 in the box: now the box says 3 and the slider something else. Using one component twice gives two separate copies of its state, and nothing connects them. A product list below them couldn't tell which price to follow.
Try it in the playground. It runs both versions with React on this page and lists every piece of state that holds the price:
function PriceInput({ label, type }) {
const [maxPrice, setMaxPrice] = useState(10);
// …the input reads and sets this copy only
}
<PriceInput label="Slider" type="range" />
<PriceInput label="Box" type="number" />in syncUp to $10
Up to $10
Where the price is kept
Slider's PriceInput: maxPrice = 10Box's PriceInput: maxPrice = 10
Two pieces of state, one in each PriceInput. They agree only because you haven't moved one without the other. Move the slider and watch the box.
With Each input keeps its own, there are two pieces of state, and moving one input turns the panel red: they disagree. With App keeps one, there is one, and you can't make them disagree, however you move them. Here is that second version:
1import { useState } from "react";2 3const teas = [4 { id: 1, name: "Green tea", price: 4 },5 { id: 2, name: "Black tea", price: 6 },6 { id: 3, name: "Mint tea", price: 9 },7];8 9function PriceInput({ label, type, value, onChange }) {10 return (11 <p>12 <label>13 {label}{" "}14 <input15 type={type}16 min="1"17 max="10"18 value={value}19 onChange={(event) => onChange(Number(event.target.value))}20 />21 </label>{" "}22 Up to ${value}23 </p>24 );25}26 27function TeaList({ maxPrice }) {28 const shown = teas.filter((tea) => tea.price <= maxPrice);29 return <p>Teas: {shown.map((tea) => tea.name).join(", ") || "none"}</p>;30}31 32export default function App() {33 const [maxPrice, setMaxPrice] = useState(10);34 return (35 <>36 <PriceInput label="Slider" type="range" value={maxPrice} onChange={setMaxPrice} />37 <PriceInput label="Box" type="number" value={maxPrice} onChange={setMaxPrice} />38 <TeaList maxPrice={maxPrice} />39 </>40 );41}Line 33 is the only state: maxPrice, in App. PriceInput on line 9 takes value and onChange props instead of keeping state. Line 18 shows value, and line 19 calls onChange with the new number. App passes the same maxPrice and setMaxPrice to both inputs (lines 36 and 37) and the price to TeaList (line 38), which keeps the teas at that price or less (line 28) and lists them. Drag the slider to 5: the box says 5, and the list shows only Green tea. Type 3 in the box: the slider jumps to 3, and the list says none.
Controlled and uncontrolled components
Compare the two PriceInputs. The first one keeps its price in its own state, so its parent can't set it or even read it. The second keeps nothing; its parent decides the price and passes it in. React developers have words for these two designs:
- An uncontrolled component keeps its important information in its own state. Its parent can't change that information. The first
FaqItemand the firstPriceInputare uncontrolled. - A controlled component gets its important information from props. Its parent decides it, and the component calls a function prop, like
onChangeoronOpen, to ask for a change. The liftedFaqItemandPriceInputare controlled byApp.
How to read the table: each row asks one question about a component, answered for each design.
| Question | An uncontrolled component | A controlled component |
|---|---|---|
| Where its important information is kept | In its own state. | In its parent, which passes it in as props. |
| Can the parent decide it? | No. Only the component can change its own state. | Yes. The parent decides it, and can change it at any time. |
| How much do you pass in? | Little. Drop it in, and it works on its own. | More. The value, and a function to call when it should change. |
| Examples in this lesson | FaqItem and PriceInput before lifting. | FaqItem and PriceInput after lifting. |
For components, these aren't strict technical terms. Most components mix both: the lifted FaqItem is controlled for isOpen but could keep, say, its own hover state. Ask, for each piece of information, "should the parent decide this?" If yes, make it a prop. If no, keep it as state.
Controlled and uncontrolled inputs
For form fields like <input>, the words are exact. An input is controlled when you give it a value prop: React makes the box show exactly that value after every render. An input without value is uncontrolled: the browser keeps the text inside the element, and defaultValue only sets the first text. Try all three ways to write a search box:
import { useState } from "react";
export default function App() {
const [query, setQuery] = useState("Sencha");
return (
<>
<input
aria-label="Search"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={() => setQuery("Oolong")}>
Set to Oolong
</button>
<p>App's query: {query}</p>
</>
);
}controlledApp's query: Sencha
Type in the box, then press Set to Oolong. Watch the box and App's line.
With value and onChange, you type and App's line follows every key; Set to Oolong changes the box too. App decides, and typing asks App to change. With value only, typing does nothing: React puts the box back to query after each key, because nothing changed query. In a project, React also prints a warning: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. With defaultValue, typing works, but App never hears about it, and Set to Oolong can't change the box.
App's state through value, and every key goes back up through onChange. Right: an uncontrolled input takes defaultValue once, and then the browser keeps the text. App still says Sencha after you typed Mint.How to read the table: each row asks one question about an input, answered for each kind.
| Question | A controlled input | An uncontrolled input |
|---|---|---|
| How you write it | <input value={query} onChange={…} /> | <input defaultValue="Sencha" />, or <input /> |
| Where the text is kept | In React state, in the component you pick. | In the browser, inside the input element. |
| What happens when you type | onChange runs, you set the state, and React shows the new text. | The browser changes the text. React doesn't need to know. |
| Can the parent change the text later? | Yes. Set the state, and the box shows it. | No. defaultValue is read only when the box first appears. |
Notice that PriceInput's <input> was controlled in both versions. Before lifting, it was controlled by PriceInput's own state; after, by App's, through two layers of props. Lifting moved who controls it. Pick one kind for an input's whole life, too. If value starts as undefined and later becomes text, React warns that a component is changing an uncontrolled input to be controlled. Start text state at "", not undefined. Forms get a lesson of their own: Forms and inputs.
One owner for each piece of state
Lifting gives every shared value one place to live. That's the rule people call a single source of truth: for each unique piece of state, one component owns it. Every other component that needs it gets it as a prop, and asks the owner to change it by calling a function prop.
It doesn't mean all state lives in one place. A tooltip's "am I showing?" can live in the tooltip. The search text lives higher, where the search box and the product list both sit under it. To find the owner of a piece of state:
- List every component that reads it or changes it.
- Find their closest common parent: the lowest component that has all of them inside it.
- Keep the state there. If that component doesn't fit, a component above it will do.
CartBadge, ProductCard, and CheckoutButton. They sit in three different branches, and the closest node above all three is App, so App keeps the count.Try it on a whole shop. Press the components that need a value, and the playground finds the owner and shows how the props travel to each one:
2 picked- keeps the state
App → Header → SearchBoxApp → ProductList
SearchBox and ProductList both sit under App, and no lower component has them all. Keep the state in App, and pass it down as props.
The search text, needed by SearchBox and ProductList, lands in App: they sit in different branches. The cart count lands in App too. But whether the search is open, which only SearchBox and CartBadge care about (the badge hides while you search), lands in Header, one level lower. And Is checkout busy? needs only CheckoutButton, so it stays there. Keep state as low as it can go: lifting further than needed makes more components render and more props to pass.
You'll often move state while you build: down when only one component turns out to need it, up when a second one does. That's normal. The Thinking in React lesson does it for a whole page.
Passing setters and handlers down
Props only go down, from parent to child. So how does a child change state that its parent owns? The parent passes a function down as a prop, and the child calls it. Here the cart count lives in App, and two product cards add to it. Click both Add buttons:
1import { useState } from "react";2 3function CartBadge({ count }) {4 return <p>Cart: {count}</p>;5}6 7function ProductCard({ name, onAdd }) {8 return (9 <li>10 {name} <button onClick={onAdd}>Add {name}</button>11 </li>12 );13}14 15export default function App() {16 const [count, setCount] = useState(0);17 return (18 <>19 <CartBadge count={count} />20 <ul>21 <ProductCard name="Green tea" onAdd={() => setCount(count + 1)} />22 <ProductCard name="Mint tea" onAdd={() => setCount(count + 1)} />23 </ul>24 </>25 );26}Line 16 keeps count in App. Line 19 passes it down to CartBadge, which shows it on line 4. Lines 21 and 22 pass each card an onAdd function. Line 10 calls it when you click. The function runs the code written in App, setCount(count + 1), and App renders again with a new count for the badge. After your two clicks, the badge says Cart: 2.
App passes count to CartBadge and a function, onAdd, to ProductCard. A click comes back up: ProductCard calls onAdd(), which runs in App and sets App's state.Data goes down as props; events come back up as calls to functions the parent gave. Prop names for these functions start with on, like onAdd, onOpen, or onChange, the way Responding to events named its handler props. You can pass two kinds of function. How to read the table: each row is one kind, what the child's call does, and when to choose it.
| What the parent passes | What the child's call does | When to choose it |
|---|---|---|
The setter itself: onChange={setMaxPrice} | onChange(5) sets maxPrice to 5. | The child reports a new value for exactly that piece of state. |
A handler: onAdd={() => setCount(count + 1)} | onAdd() tells the parent what happened. | The parent should decide what the event means for its state. |
The price inputs got the setter itself, onChange={setMaxPrice}: their job is to pick a new price, and the call onChange(5) says exactly that. The product cards got a handler, onAdd: a card only knows "someone clicked Add", and App decides what that means. If the cart later keeps a list of items instead of a number, only App's handler changes; ProductCard stays the same. Press Edit & run and change line 21 to onAdd={() => setCount(count + 2)}: Green tea now adds 2, and the card's code didn't change.
A shop whose filters, list, and count agree
A real filter bar puts all of this together: a search box, buttons for the kind of tea, a count, a Clear filters button, and the list. Type s, press black, then Clear filters:
1import { useState } from "react";2 3const teas = [4 { id: 1, name: "Sencha", kind: "green" },5 { id: 2, name: "Jasmine", kind: "green" },6 { id: 3, name: "Assam", kind: "black" },7 { id: 4, name: "Earl Grey", kind: "black" },8];9 10function SearchBox({ query, onQueryChange }) {11 return (12 <input13 placeholder="Search teas"14 value={query}15 onChange={(event) => onQueryChange(event.target.value)}16 />17 );18}19 20function KindChips({ kind, onKindChange }) {21 return (22 <p>23 {["all", "green", "black"].map((name) => (24 <button key={name} aria-pressed={kind === name} onClick={() => onKindChange(name)}>25 {name}26 </button>27 ))}28 </p>29 );30}31 32function TeaList({ items }) {33 return (34 <ul>35 {items.map((tea) => (36 <li key={tea.id}>{tea.name}</li>37 ))}38 </ul>39 );40}41 42export default function App() {43 const [query, setQuery] = useState("");44 const [kind, setKind] = useState("all");45 const shown = teas.filter(46 (tea) =>47 (kind === "all" || tea.kind === kind) &&48 tea.name.toLowerCase().includes(query.toLowerCase())49 );50 51 function clearFilters() {52 setQuery("");53 setKind("all");54 }55 56 return (57 <>58 <SearchBox query={query} onQueryChange={setQuery} />59 <KindChips kind={kind} onKindChange={setKind} />60 <p>61 {shown.length} of {teas.length} teas{" "}62 <button onClick={clearFilters}>Clear filters</button>63 </p>64 <TeaList items={shown} />65 </>66 );67}App owns both filters: query on line 43 and kind on line 44. SearchBox (lines 10 to 18) is a controlled input: it shows query and reports each key through onQueryChange on line 15. KindChips (lines 20 to 30) marks the chosen kind with aria-pressed and reports clicks through onKindChange on line 24. Lines 45 to 49 work out the teas to show from both filters, with filter, on every render; they aren't state, because they can always be worked out from query, kind, and teas.
Typing s leaves Sencha, Jasmine, and Assam: 3 of 4 teas. black leaves Assam alone. Clear filters (lines 51 to 54) sets both filters back, and the box empties too, because it shows query. If SearchBox kept its own text, Clear would reset App's copy and leave the old text in the box. Exercise 5 has that bug for you to fix.
- Whether a tooltip is showing, read only by that tooltip
- The search text, read by the search box and the product list
- Which tab is open, when only one tab can be open
- The text in a comment box, until you press Send
- The cart count, shown in the header and changed by every Add button
- Whether one card's details are open, when cards open on their own
- The chosen size, picked in a size menu and shown in the price line beside it
- Whether the mouse is over one button
Sort each piece of state: can it stay in the component that has it, or should it be lifted to a parent?
Common mix-ups
- "Two copies of the same component share their state." No. Each copy gets its own state. Two
PriceInputs each kept their ownmaxPrice. - "A child can set its parent's state if it knows the name." No. State belongs to the component that called
useState. A child changes it only by calling a function the parent passed down. - "Lift all state to App, to be safe." No. Keep each piece in the closest common parent of the components that need it. Higher means more props and more components rendering for no reason.
- "Sync two components by keeping a copy in each and setting both." No. Any path that sets one and forgets the other leaves them out of sync. Keep one copy and pass it down.
- "An input with
valueis controlled, so typing in it changes the state." No. Typing only callsonChange. Your code must set the state there, or the box can't change. - "
defaultValueupdates the box when the state changes." No. It's read only when the input first appears. To change the text later from code, make the input controlled.
Practice exercises
For each one, first ask which components need the state, and where their closest common parent is. Press Edit & run to try your changes.
Predict first, then run it: you type 4 in the Box. What does the slider's line say?
import { useState } from "react";
function PriceInput({ label, type }) {
const [maxPrice, setMaxPrice] = useState(10);
return (
<p>
<label>
{label}{" "}
<input
type={type}
min="1"
max="10"
value={maxPrice}
onChange={(event) => setMaxPrice(Number(event.target.value))}
/>
</label>{" "}
Up to ${maxPrice}
</p>
);
}
export default function App() {
return (
<>
<PriceInput label="Slider" type="range" />
<PriceInput label="Box" type="number" />
</>
);
}1import { useState } from "react";2 3function PriceInput({ label, type }) {4 const [maxPrice, setMaxPrice] = useState(10);5 return (6 <p>7 <label>8 {label}{" "}9 <input10 type={type}11 min="1"12 max="10"13 value={maxPrice}14 onChange={(event) => setMaxPrice(Number(event.target.value))}15 />16 </label>{" "}17 Up to ${maxPrice}18 </p>19 );20}21 22export default function App() {23 return (24 <>25 <PriceInput label="Slider" type="range" />26 <PriceInput label="Box" type="number" />27 </>28 );29}The two PriceInputs don't share anything, so typing 4 in the Box changes only the Box's maxPrice. The slider's line still says Up to $10.
The badge should count the products added to the cart. Which component should keep count? Then try to wire it up.
function CartBadge() {
return <p>Cart: 0</p>;
}
function Header() {
return (
<header>
<h1>Tea shop</h1>
<CartBadge />
</header>
);
}
function ProductCard({ name }) {
return <li>{name} <button>Add {name}</button></li>;
}
function ProductList() {
return (
<ul>
<ProductCard name="Green tea" />
<ProductCard name="Mint tea" />
</ul>
);
}
export default function App() {
return (
<>
<Header />
<ProductList />
</>
);
}1import { useState } from "react";2 3function CartBadge({ count }) {4 return <p>Cart: {count}</p>;5}6 7function Header({ count }) {8 return (9 <header>10 <h1>Tea shop</h1>11 <CartBadge count={count} />12 </header>13 );14}15 16function ProductCard({ name, onAdd }) {17 return <li>{name} <button onClick={onAdd}>Add {name}</button></li>;18}19 20function ProductList({ onAdd }) {21 return (22 <ul>23 <ProductCard name="Green tea" onAdd={onAdd} />24 <ProductCard name="Mint tea" onAdd={onAdd} />25 </ul>26 );27}28 29export default function App() {30 const [count, setCount] = useState(0);31 return (32 <>33 <Header count={count} />34 <ProductList onAdd={() => setCount(count + 1)} />35 </>36 );37}CartBadge and ProductCard are in different branches, and the closest component above both is App. So App keeps count, passes it through Header to CartBadge, and passes onAdd through ProductList to each card.
All three answers can be open together. Make it one at a time, starting with none open. After your fix, click the three questions in order. How many answers are showing?
1import { useState } from "react";2 3function FaqItem({ question, children }) {4 const [isOpen, setIsOpen] = useState(false);5 return (6 <section>7 <button onClick={() => setIsOpen(true)}>{question}</button>8 {isOpen && <p>{children}</p>}9 </section>10 );11}12 13export default function App() {14 return (15 <>16 <FaqItem question="How do I brew it?">3 minutes at 80 °C.</FaqItem>17 <FaqItem question="Does it have caffeine?">A little, less than coffee.</FaqItem>18 <FaqItem question="How long does it keep?">A year, in a closed tin.</FaqItem>19 </>20 );21}1import { useState } from "react";2 3function FaqItem({ question, children, isOpen, onOpen }) {4 return (5 <section>6 <button onClick={onOpen}>{question}</button>7 {isOpen && <p>{children}</p>}8 </section>9 );10}11 12export default function App() {13 const [openId, setOpenId] = useState(null);14 const itemProps = (id) => ({15 isOpen: openId === id,16 onOpen: () => setOpenId(id),17 });18 return (19 <>20 <FaqItem question="How do I brew it?" {...itemProps("brew")}>21 3 minutes at 80 °C.22 </FaqItem>23 <FaqItem question="Does it have caffeine?" {...itemProps("caffeine")}>24 A little, less than coffee.25 </FaqItem>26 <FaqItem question="How long does it keep?" {...itemProps("keep")}>27 A year, in a closed tin.28 </FaqItem>29 </>30 );31}App keeps one openId. itemProps("brew") gives an item both props at once with spread: whether it's open, and a function that opens it. Clicking a question replaces openId, so the last one you clicked is the only one open.
The preview never shows the name you type. Fix it. After your fix, what does the preview say once you type Asha?
1import { useState } from "react";2 3function GiftForm() {4 const [to, setTo] = useState("");5 return (6 <label>7 To <input value={to} onChange={(event) => setTo(event.target.value)} />8 </label>9 );10}11 12function CardPreview() {13 return <p>Card preview: To: (nobody yet)</p>;14}15 16export default function App() {17 return (18 <>19 <GiftForm />20 <CardPreview />21 </>22 );23}1import { useState } from "react";2 3function GiftForm({ to, onToChange }) {4 return (5 <label>6 To <input value={to} onChange={(event) => onToChange(event.target.value)} />7 </label>8 );9}10 11function CardPreview({ to }) {12 return <p>Card preview: To: {to || "(nobody yet)"}</p>;13}14 15export default function App() {16 const [to, setTo] = useState("");17 return (18 <>19 <GiftForm to={to} onToChange={setTo} />20 <CardPreview to={to} />21 </>22 );23}Now App owns to. The form is a controlled input that reports each key with onToChange, and the preview reads the same to. Type Asha, and it says Card preview: To: Asha.
Type mint, then press Clear: the line resets, but the box doesn't. Make the box follow App. After your fix, what is in the box after Clear?
1import { useState } from "react";2 3function SearchBox({ onSearch }) {4 const [text, setText] = useState("");5 return (6 <input7 placeholder="Search teas"8 value={text}9 onChange={(event) => {10 setText(event.target.value);11 onSearch(event.target.value);12 }}13 />14 );15}16 17export default function App() {18 const [query, setQuery] = useState("");19 return (20 <>21 <SearchBox onSearch={setQuery} />22 <button onClick={() => setQuery("")}>Clear</button>23 <p>Searching for: {query || "everything"}</p>24 </>25 );26}1import { useState } from "react";2 3function SearchBox({ value, onChange }) {4 return (5 <input6 placeholder="Search teas"7 value={value}8 onChange={(event) => onChange(event.target.value)}9 />10 );11}12 13export default function App() {14 const [query, setQuery] = useState("");15 return (16 <>17 <SearchBox value={query} onChange={setQuery} />18 <button onClick={() => setQuery("")}>Clear</button>19 <p>Searching for: {query || "everything"}</p>20 </>21 );22}With one copy, in App, the box is controlled by query. Clear sets query to an empty string, and the box shows it: the box is empty, and the line says Searching for: everything.
Type 3 in the box, then press Reset: the line says Up to $10, and the box still says 3. Fix it. After your fix, what does the box show after Reset?
1import { useState } from "react";2 3function PriceBox({ value, onChange }) {4 const [text, setText] = useState(value);5 return (6 <input7 aria-label="Max price"8 type="number"9 value={text}10 onChange={(event) => {11 setText(event.target.value);12 onChange(Number(event.target.value));13 }}14 />15 );16}17 18export default function App() {19 const [maxPrice, setMaxPrice] = useState(10);20 return (21 <>22 <PriceBox value={maxPrice} onChange={setMaxPrice} />23 <button onClick={() => setMaxPrice(10)}>Reset</button>24 <p>Up to ${maxPrice}</p>25 </>26 );27}1import { useState } from "react";2 3function PriceBox({ value, onChange }) {4 return (5 <input6 aria-label="Max price"7 type="number"8 value={value}9 onChange={(event) => onChange(Number(event.target.value))}10 />11 );12}13 14export default function App() {15 const [maxPrice, setMaxPrice] = useState(10);16 return (17 <>18 <PriceBox value={maxPrice} onChange={setMaxPrice} />19 <button onClick={() => setMaxPrice(10)}>Reset</button>20 <p>Up to ${maxPrice}</p>21 </>22 );23}The box kept its own copy, text, made from the prop once. Reset changed App's maxPrice, but not the copy. Without the copy, the box shows App's value: type 3, press Reset, and it shows 10.
Check your understanding
For each question, ask where each piece of state lives and who can change it.
Question 1 of 8Two sibling components must always show the same value. Where should its state live?
Choose an answer to see the explanation.
Question 2 of 8You click the first button twice and the second once. What do the two buttons show?
Read the code, then predictPop out in the code editor (opens in a new tab)JSXimport { useState } from "react"; function Counter() { const [clicks, setClicks] = useState(0); return <button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>; } export default function App() { return ( <> <Counter /> <Counter /> </> ); }Choose an answer to see the explanation.
Question 3 of 8In this version, you click the first button twice and the second once. What do the buttons show?
Read the code, then predictPop out in the code editor (opens in a new tab)JSXimport { useState } from "react"; function Counter({ clicks, onClick }) { return <button onClick={onClick}>Clicks: {clicks}</button>; } export default function App() { const [clicks, setClicks] = useState(0); const add = () => setClicks(clicks + 1); return ( <> <Counter clicks={clicks} onClick={add} /> <Counter clicks={clicks} onClick={add} /> </> ); }Choose an answer to see the explanation.
Question 4 of 8Which of these inputs is controlled?
Choose an answer to see the explanation.
Question 5 of 8An input has
value={query}and noonChange. What happens when you type in it?Choose an answer to see the explanation.
Question 6 of 8A
PriceInputgetsvalueandonChangefrom its parent and keeps no state of its own. What is it?Choose an answer to see the explanation.
Question 7 of 8How does
ProductCardchange the cart count thatAppkeeps?Choose an answer to see the explanation.
Question 8 of 8What does "a single source of truth" mean for state?
Choose an answer to see the explanation.
Key takeaways
- Each copy of a component has its own state, so two components that must agree can't each keep a copy.
- Lift the state up: remove it from the children, keep it in their closest common parent, and pass it down as props.
- Children change the parent's state by calling functions it passes down, named
onSomething. - An uncontrolled component keeps its important information in its own state; a controlled one gets it from props, so its parent decides.
- An input with
valueis controlled: React shows exactly that value. WithdefaultValueor nothing, the browser keeps the text. - Each piece of state has one owner, as low in the tree as it can be: a single source of truth.
Remember the one-liner.
When two components must agree, keep one copy of the state in their closest common parent, pass it down, and let the children call back up.
Coming next: Preserving and resetting state. State belongs to a place in the tree, and it shows when React keeps state, when it throws it away, and how a key resets it on purpose.