Preserving and resetting state
React keeps state by its place in the tree. See when typed text survives a render or vanishes, why nested components reset, and how a key resets on purpose.
- 01Predict when state is keptRead what a component returns and say whether React keeps a child's state or throws it away after a render.
- 02Avoid accidental resetsKeep wrappers and component types stable from render to render, and define every component at the top level of its file.
- 03Reset state on purposeGive a component a
key, likekey={contact.id}, to start it fresh, and lift up state that must outlive it.
A gift message that disappears
A checkout has a note for the delivery driver and, when the order is a gift, a gift message. Type something in both boxes in the result. Then untick This is a gift, and tick it again:
import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} export default function App() { const [isGift, setIsGift] = useState(true); return ( <> <NoteBox label="Note for the driver" /> {isGift && <NoteBox label="Gift message" />} <label> <input type="checkbox" checked={isGift} onChange={(event) => setIsGift(event.target.checked)} /> This is a gift </label> </> );}NoteBox (lines 3 to 13) keeps its own text on line 4, and line 9 is a controlled input that shows it. App renders the driver's NoteBox on line 19 every time, and the gift NoteBox on line 20 only while isGift is true. Lines 21 to 28 are the checkbox that sets isGift.
The driver's note survives. The gift message comes back empty. Both boxes are the same component, and App rendered again both times. The difference is that the gift box left the page for a moment, and the driver's box never did. React decides whether to keep a component's state by where the component is in the render tree, and whether it stays there.
NoteBox's text at its place under App: first child, second child. Untick the box, and the second place is empty, so React throws its text away. Tick it again, and a new NoteBox starts there with no text.In a classroom, papers belong to a desk, not to a student. Whoever sits at the second desk in the first row finds what was left there. If the desk is carried out of the room, its papers go with it, and the desk that comes back later is empty. A name card on a desk marks it for one student; change the card, and the desk is cleared for the next one.
- In real life: A desk: row 1, second seat
- In React: A place in the render tree: App's second child
- In real life: The papers left on that desk
- In React: The state React keeps for that place
- In real life: A new student sits there and finds the papers
- In React: Same component, same place, new props: state kept
- In real life: The desk is carried out; a new one arrives empty
- In React: The component leaves the tree and comes back: state reset
- In real life: A name card on the desk changes, so the desk is cleared
- In React: A new key: state reset
React keeps a component's state for as long as the same component stays in the same place in the tree. If it's removed, or a different component takes its place, React throws the state away. A key lets you decide what counts as "the same".
State belongs to a place in the tree
It looks as if text lives inside NoteBox. It doesn't. NoteBox is a function, and its local variables start over each time React calls it. When it calls useState(""), it asks React for the value React is keeping for it. React keeps that value with the component's place in the render tree.
A place is the path from the top: "App's first child", "App's second child". In the checkout, the driver's note is App's first child, and the gift message is its second. That's why two NoteBoxes on one page each have their own text, as the State lesson showed: they're two places.
Each time a component renders, React compares the new tree with the last one, place by place:
- Same component in the same place: React keeps its state and gives it the new props.
- Nothing in a place that had a component: React removes the component and throws its state away.
- A component in a place that was empty: React makes a new one, with fresh state from
useState's first value. - A different component in a place: React removes the old one, state and all, and makes the new one.
Line 20, {isGift && <NoteBox label="Gift message" />}, gives the second place either a NoteBox or false, which renders nothing. When you untick the box, the second place is empty, so React removes the gift NoteBox and its text. When you tick it, the place gets a NoteBox again: a new one, starting at "". The driver's note never left its place, so its text stays.
Inside React, each place in the tree is an object called a fiber, and your state is stored on it. When React removes the fiber, the state goes with it. You don't need those details to use state; the Fiber and Reconciliation lessons, at the end of the course, show the code that does this comparison.
Same component, same place: React keeps the state
Here is a box that switches between a gift message and a note for the driver. Type Happy birthday in it, then press Switch note:
import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} export default function App() { const [isGift, setIsGift] = useState(true); return ( <> {isGift ? ( <NoteBox label="Gift message" /> ) : ( <NoteBox label="Note for the driver" /> )} <button onClick={() => setIsGift(!isGift)}>Switch note</button> </> );}Lines 19 to 23 pick between two NoteBox tags with a ternary, ?:. Line 20 is the gift message and line 22 the driver's note. Line 24 flips isGift.
The label changes to Note for the driver, but your Happy birthday stays in the box. You wrote two tags, yet both put a NoteBox in the same place: App's first child. React never sees your ?:; it only sees the tree you return. Before the click, that tree had a NoteBox first; after, it still does. Same component, same place, so React keeps the state and only passes a new label.
The same happens with two returns in an if. Try it:
import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} export default function App() { const [isGift, setIsGift] = useState(true); if (isGift) { return ( <> <NoteBox label="Gift message" /> <button onClick={() => setIsGift(false)}>Switch note</button> </> ); } return ( <> <NoteBox label="Note for the driver" /> <button onClick={() => setIsGift(true)}>Switch note</button> </> );}Line 17 decides which of two returns runs. Line 20 returns a gift NoteBox; line 27 returns a driver's one. Both are App's first child, so your text survives Switch note here too.
NoteBox, so React keeps its text and only changes the label. Right: a LongNote takes the place of the NoteBox, so React removes the NoteBox and its text, and LongNote starts empty.Here, keeping the text is a bug: a birthday message shouldn't turn into instructions for the driver. You'll fix it with a key in a later section. Often, though, keeping state is what you want: a counter that changes color, or a form that gets a new title, shouldn't forget what you typed.
A different component or parent resets it
Now the shop adds a bigger box for long messages. Type a message, then press Longer message, and then Short message:
import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} function LongNote() { const [text, setText] = useState(""); return ( <p> <textarea aria-label="Long gift message" value={text} onChange={(event) => setText(event.target.value)} /> </p> );} export default function App() { const [isLong, setIsLong] = useState(false); return ( <> {isLong ? <LongNote /> : <NoteBox label="Gift message" />} <button onClick={() => setIsLong(!isLong)}> {isLong ? "Short message" : "Longer message"} </button> </> );}LongNote (lines 15 to 26) keeps its own text too, in a <textarea>. Line 32 puts a LongNote or a NoteBox in App's first place. Your text is gone after the first press, and gone again after the second. A LongNote isn't a NoteBox, even with the same kind of state, so React removes one and makes the other from scratch.
A different element above a component does the same, for everything inside it. This shop highlights the box by wrapping it in a div with a class. Type a message, then press Highlight:
import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} export default function App() { const [isHighlighted, setIsHighlighted] = useState(false); return ( <> {isHighlighted ? ( <div className="highlight"> <NoteBox label="Gift message" /> </div> ) : ( <section> <NoteBox label="Gift message" /> </section> )} <button onClick={() => setIsHighlighted(!isHighlighted)}>Highlight</button> </> );}Lines 21 and 25 are the same NoteBox, with the same label. But line 20 wraps it in a div, and line 24 in a section. When the section becomes a div, React removes the section and its whole branch of the tree, and the NoteBox inside goes with it. The text vanishes.
div where the section was. A different element type in that place means React removes the section and everything under it, including the NoteBox and its text. The NoteBox under the new div starts empty.The rule of thumb: to keep state, keep the tree's shape the same from one render to the next. Here, use one div and change only its class: <div className={isHighlighted ? "highlight" : ""}>. An attribute change keeps the element, so the text stays. Exercise 3 asks you to make that change.
Try every kind of change in the playground. It runs the note box with React on this page, in the structure the code shows. Type in the box, press Switch, and see whether your text is still there:
{isOn ? (
<NoteBox label="Note for the driver" />
) : (
<NoteBox label="Gift message" />
)}0 switchesTree now: App › NoteBox
Type a few words in the box, then press Switch. Watch whether your text is still there.
Only New label keeps your text. Different component, Different wrapper, Different key, and Defined inside App all start empty. How to read the table: each row is one thing that can change between two renders, whether the NoteBox keeps its text, and why.
| What changes between renders | Does the text stay? | Why |
|---|---|---|
Its props, like label | Kept. | The same component in the same place: React gives it the new props and keeps its state. |
Which branch of a ?: or an if returns it | Kept, when both branches put the same component in the same place. | React sees only the tree you return, not which line of your code made it. |
The component, NoteBox to LongNote | Reset. | A different component in that place: React removes the old one and its state. |
An element above it, section to div | Reset, with everything inside. | React removes the whole branch under the element that changed. |
Its key | Reset. | A new key tells React it's a different component. |
A className on the element above it | Kept. | The element is still a div; only one of its attributes changed. |
Never define a component inside another
The Your first component lesson warned you not to write a component inside another. Here is what goes wrong. Type a review, then press Helpful:
import { useState } from "react"; export default function App() { const [helpful, setHelpful] = useState(0); function ReviewBox() { const [text, setText] = useState(""); return ( <input aria-label="Your review" value={text} onChange={(event) => setText(event.target.value)} /> ); } return ( <> <ReviewBox /> <button onClick={() => setHelpful(helpful + 1)}>Helpful ({helpful})</button> </> );}Your review vanishes. ReviewBox (lines 6 to 15) is written inside App, so line 6 runs every time App runs, and each time it makes a new function. Pressing Helpful sets helpful on line 20, so App renders again and makes a new ReviewBox. On line 19, React finds a component in the first place that is a different function from last time, so to React it's a different component. It removes the old one, with your text, and makes a new one.
The fix is to move the function out, to the top level of the file:
import { useState } from "react"; function ReviewBox() { const [text, setText] = useState(""); return ( <input aria-label="Your review" value={text} onChange={(event) => setText(event.target.value)} /> );} export default function App() { const [helpful, setHelpful] = useState(0); return ( <> <ReviewBox /> <button onClick={() => setHelpful(helpful + 1)}>Helpful ({helpful})</button> </> );}Now ReviewBox (lines 3 to 12) is made once, when the file loads. Every render of App puts the same function on line 18, so React keeps its state. Type, press Helpful, and your review stays. If an inner component needs a value from App, pass it as a prop instead of reaching for App's variables.
If the review's text lived in App, as a lifted input's does, each key would render App and make a new ReviewBox. The box would lose its focus after every letter, so you could type only one letter at a time. React also throws away and rebuilds the box's DOM each time, which is slow.
Resetting state on purpose with a key
Sometimes React keeping state is the bug. In this support chat, type a message to Asha, then click Ben:
import { useState } from "react"; const contacts = [ { id: "asha", name: "Asha" }, { id: "ben", name: "Ben" }, { id: "chen", name: "Chen" },]; function ChatWindow({ contact }) { const [draft, setDraft] = useState(""); return ( <section> <h3>Chat with {contact.name}</h3> <input aria-label="Message" placeholder={"Message " + contact.name} value={draft} onChange={(event) => setDraft(event.target.value)} /> </section> );} export default function App() { const [contact, setContact] = useState(contacts[0]); return ( <> {contacts.map((item) => ( <button key={item.id} onClick={() => setContact(item)}> {item.name} </button> ))} <ChatWindow contact={contact} /> </> );}The heading says Chat with Ben, but your message to Asha is still in the box, one click from going to the wrong person. ChatWindow (lines 9 to 22) keeps draft on line 10. Line 33 always renders one ChatWindow in the same place; only its contact prop changes. Same component, same place: React keeps the draft. (The UI as a tree lesson met this chat, too.)
You want each person's chat to be a different component to React. A key says that. You used keys in Rendering lists to tell list items apart, and they work on any component: React keeps state only while both the type and the key stay the same.
import { useState } from "react"; const contacts = [ { id: "asha", name: "Asha" }, { id: "ben", name: "Ben" }, { id: "chen", name: "Chen" },]; function ChatWindow({ contact }) { const [draft, setDraft] = useState(""); return ( <section> <h3>Chat with {contact.name}</h3> <input aria-label="Message" placeholder={"Message " + contact.name} value={draft} onChange={(event) => setDraft(event.target.value)} /> </section> );} export default function App() { const [contact, setContact] = useState(contacts[0]); return ( <> {contacts.map((item) => ( <button key={item.id} onClick={() => setContact(item)}> {item.name} </button> ))} <ChatWindow key={contact.id} contact={contact} /> </> );}Line 33 is the only change: <ChatWindow key={contact.id} contact={contact} />. When you click Ben, the key changes from "asha" to "ben". React removes Asha's ChatWindow, with her draft, and makes a new one for Ben, with an empty box. Click back to Asha: her draft is gone too, because her window was removed. The practical section keeps it.
ChatWindow in the same place, so Asha's draft is still in it. Right: with key={contact.id}, the key changes from asha to ben. React removes Asha's window and its draft, and makes a new one for Ben.A key isn't global. React compares keys only among the children of one parent, so "asha" can be a key in a contacts list and in a chat list at once. The key decides which child of this parent a component is.
There is a second way to reset: render the choices in different places. It suits a small, fixed number of choices:
{isAsha && <ChatWindow contact={contacts[0]} />}
{!isAsha && <ChatWindow contact={contacts[1]} />}Line 1 is App's first place and line 2 its second. Asha's window always lives in the first, and Ben's in the second, so switching removes one and makes the other. It's a fragment of an app, so it doesn't run on its own. How to read the table: each row is a way to reset, what it looks like, and when to use it.
| Way to reset | What it looks like | When to use it |
|---|---|---|
| Render it in two different places | {isAsha && <ChatWindow … />} and {!isAsha && <ChatWindow … />} | A few fixed choices, like two people. |
| Give it a key | <ChatWindow key={contact.id} … /> | Any number of choices, like contacts from a list. |
A key is also the easy way to clear a whole form. Keep a number in the parent, pass it as the form's key, and add one to it: the form, and every box in it, starts over. Exercise 5 does that.
A chat that keeps a draft for each contact
The key gave each person a fresh box, but a real chat app remembers what you half-wrote to each person. React throws state away when a component leaves the tree, so the drafts must live in a component that stays: App. Lift them up. Type to Asha, click Ben, then click Asha (draft):
import { useState } from "react"; const contacts = [ { id: "asha", name: "Asha" }, { id: "ben", name: "Ben" }, { id: "chen", name: "Chen" },]; function ChatWindow({ contact, draft, onDraftChange, onSend }) { return ( <section> <h3>Chat with {contact.name}</h3> <input aria-label="Message" value={draft} onChange={(event) => onDraftChange(event.target.value)} /> <button onClick={onSend}>Send</button> </section> );} export default function App() { const [contact, setContact] = useState(contacts[0]); const [drafts, setDrafts] = useState({}); const draft = drafts[contact.id] ?? ""; function changeDraft(text) { setDrafts({ ...drafts, [contact.id]: text }); } return ( <> {contacts.map((item) => ( <button key={item.id} onClick={() => setContact(item)}> {item.name} {drafts[item.id] ? " (draft)" : ""} </button> ))} <ChatWindow contact={contact} draft={draft} onDraftChange={changeDraft} onSend={() => changeDraft("")} /> </> );}Line 25 keeps drafts, an object with one entry per contact id, like { asha: "Hi Asha" }. Line 26 reads the open contact's draft, or "" when there is none. changeDraft (lines 28 to 30) makes a new object with spread and replaces that one contact's entry. ChatWindow has no state now: line 15 shows the draft it's given, and line 16 reports each key. Line 37 marks contacts with a draft, and Send on line 18 clears it.
Switching contacts no longer loses anything, because the drafts never lived in ChatWindow. How to read the table: each row is a way to keep state when a component leaves the screen, how it works, and when it fits.
| Way to keep it | How | When it fits |
|---|---|---|
| Keep it in the tree and hide it with CSS | Render every chat, and hide all but one with hidden. | A few small parts. Many hidden parts slow the page down. |
| Lift the state up | App keeps a draft for each contact, by its id. | Most cases: the parent remembers, whichever chat is open. |
| Save it outside React | Write each draft to localStorage, and read it back when the chat opens. | Drafts that should survive closing the tab. |
<Activity mode="hidden"> | Hide part of the page while React keeps its state (added in React 19.2). | Tabs and pages the reader comes back to. The Activity lesson covers it. |
The last row is new: <Activity> came out in React 19.2. Its hidden mode hides a part of the page and keeps its state, so a tab you come back to still has what you typed. The Activity lesson covers it.
<NoteBox label="Gift" />becomes<NoteBox label="Driver" />{a ? <NoteBox /> : <NoteBox />}whenachanges<NoteBox />becomes<LongNote />in the same place<div><NoteBox /></div>becomes<section><NoteBox /></section>{show && <NoteBox />}whenshowgoes false, then true<NoteBox key={id} />whenidchanges<div className={a ? "on" : ""}><NoteBox /></div>whenachangesNoteBoxis written insideApp, andApprenders again
Sort each change: after it, does the NoteBox keep its text, or start empty?
Common mix-ups
- "State lives inside the component function." No. React keeps it, tied to the component's place in the tree. The function asks for it with
useStateon every render. - "A new prop resets a component's state." No. New props reach the same component in the same place, and its state stays. That's why the draft followed you to Ben.
- "Two different JSX tags are two different components." No. If both put the same component in the same place, React sees one component that stayed.
- "Hiding a component with
&&keeps its state for later." No.falseremoves it from the tree, and its state goes with it. - "Only the component that changed loses its state." No. When an element changes type, React removes everything under it, including components that didn't change.
- "Keys are only for lists." No. Any component can take a key, and a new key resets it.
- "Keys must be unique across the whole app." No. They only need to be unique among the children of one parent.
Practice exercises
For each one, draw the tree before and after the click. Ask: is the same component in the same place, with the same key? Press Edit & run to check.
Predict first: you type Hi in the box, then press Show banner. The banner appears above the box. What is in the box?
import { useState } from "react";
function NoteBox({ label }) {
const [text, setText] = useState("");
return (
<p>
<label>
{label}{" "}
<input value={text} onChange={(event) => setText(event.target.value)} />
</label>
</p>
);
}
function Banner() {
return <p>Free delivery on orders over $20!</p>;
}
export default function App() {
const [showBanner, setShowBanner] = useState(false);
return (
<>
{showBanner && <Banner />}
<NoteBox label="Gift message" />
<button onClick={() => setShowBanner(!showBanner)}>Show banner</button>
</>
);
}import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} function Banner() { return <p>Free delivery on orders over $20!</p>;} export default function App() { const [showBanner, setShowBanner] = useState(false); return ( <> {showBanner && <Banner />} <NoteBox label="Gift message" /> <button onClick={() => setShowBanner(!showBanner)}>Show banner</button> </> );}{showBanner && <Banner />} always fills the first place, with false or a Banner, so the NoteBox is always the second child. Same component, same place: the box still says Hi.
Predict first: you press the tea button 3 times, then Switch drink twice. What does the tea button say?
import { useState } from "react";
function CupCounter() {
const [cups, setCups] = useState(0);
return <button onClick={() => setCups(cups + 1)}>Cups of tea: {cups}</button>;
}
function GlassCounter() {
const [glasses, setGlasses] = useState(0);
return <button onClick={() => setGlasses(glasses + 1)}>Glasses of water: {glasses}</button>;
}
export default function App() {
const [showTea, setShowTea] = useState(true);
return (
<>
{showTea ? <CupCounter /> : <GlassCounter />}
<button onClick={() => setShowTea(!showTea)}>Switch drink</button>
</>
);
}import { useState } from "react"; function CupCounter() { const [cups, setCups] = useState(0); return <button onClick={() => setCups(cups + 1)}>Cups of tea: {cups}</button>;} function GlassCounter() { const [glasses, setGlasses] = useState(0); return <button onClick={() => setGlasses(glasses + 1)}>Glasses of water: {glasses}</button>;} export default function App() { const [showTea, setShowTea] = useState(true); return ( <> {showTea ? <CupCounter /> : <GlassCounter />} <button onClick={() => setShowTea(!showTea)}>Switch drink</button> </> );}A different component in the same place resets it. Switch drink removes CupCounter with its count of 3, and the second switch makes a new CupCounter that starts at 0.
Highlight wipes the gift message. Fix it. After your fix, type Hi and press Highlight. What is in the box?
import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} export default function App() { const [isHighlighted, setIsHighlighted] = useState(false); return ( <> {isHighlighted ? ( <div className="highlight"> <NoteBox label="Gift message" /> </div> ) : ( <section> <NoteBox label="Gift message" /> </section> )} <button onClick={() => setIsHighlighted(!isHighlighted)}>Highlight</button> </> );}import { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> );} export default function App() { const [isHighlighted, setIsHighlighted] = useState(false); return ( <> <div className={isHighlighted ? "highlight" : ""}> <NoteBox label="Gift message" /> </div> <button onClick={() => setIsHighlighted(!isHighlighted)}>Highlight</button> </> );}Now the place always holds a div; only its class changes. React keeps the div and the NoteBox inside it, so Hi stays when you press Highlight.
Clicking Helpful wipes the review. Fix it. After your fix, type Great tea and press Helpful. What is in the box?
import { useState } from "react"; export default function App() { const [helpful, setHelpful] = useState(0); function ReviewBox() { const [text, setText] = useState(""); return ( <input aria-label="Your review" value={text} onChange={(event) => setText(event.target.value)} /> ); } return ( <> <ReviewBox /> <button onClick={() => setHelpful(helpful + 1)}>Helpful ({helpful})</button> </> );}import { useState } from "react"; function ReviewBox() { const [text, setText] = useState(""); return ( <input aria-label="Your review" value={text} onChange={(event) => setText(event.target.value)} /> );} export default function App() { const [helpful, setHelpful] = useState(0); return ( <> <ReviewBox /> <button onClick={() => setHelpful(helpful + 1)}>Helpful ({helpful})</button> </> );}At the top level, ReviewBox is made once, so every render of App puts the same component in the same place, and React keeps the review.
Start over should empty the whole form, but nothing happens. Fix it with one change, without touching GiftForm. After your fix, fill in both boxes and press Start over. What does the Card line say?
import { useState } from "react"; function GiftForm() { const [to, setTo] = useState(""); const [message, setMessage] = useState(""); return ( <> <input aria-label="To" value={to} onChange={(event) => setTo(event.target.value)} /> <input aria-label="Message" value={message} onChange={(event) => setMessage(event.target.value)} /> <p>Card: {to || message ? to + ": " + message : "(empty)"}</p> </> );} export default function App() { const [round, setRound] = useState(0); return ( <> <GiftForm /> <button onClick={() => setRound(round + 1)}>Start over</button> </> );}import { useState } from "react"; function GiftForm() { const [to, setTo] = useState(""); const [message, setMessage] = useState(""); return ( <> <input aria-label="To" value={to} onChange={(event) => setTo(event.target.value)} /> <input aria-label="Message" value={message} onChange={(event) => setMessage(event.target.value)} /> <p>Card: {to || message ? to + ": " + message : "(empty)"}</p> </> );} export default function App() { const [round, setRound] = useState(0); return ( <> <GiftForm key={round} /> <button onClick={() => setRound(round + 1)}>Start over</button> </> );}Each press changes round, so the form's key changes, and React makes a new GiftForm with both boxes empty. The card line says Card: (empty).
Each chat starts empty, but now a half-written message is lost when you switch away. Keep a draft for each contact. After your fix, type Hi Asha, click Ben, then Asha. What is in the box?
import { useState } from "react"; const contacts = [ { id: "asha", name: "Asha" }, { id: "ben", name: "Ben" }, { id: "chen", name: "Chen" },]; function ChatWindow({ contact }) { const [draft, setDraft] = useState(""); return ( <section> <h3>Chat with {contact.name}</h3> <input aria-label="Message" placeholder={"Message " + contact.name} value={draft} onChange={(event) => setDraft(event.target.value)} /> </section> );} export default function App() { const [contact, setContact] = useState(contacts[0]); return ( <> {contacts.map((item) => ( <button key={item.id} onClick={() => setContact(item)}> {item.name} </button> ))} <ChatWindow key={contact.id} contact={contact} /> </> );}import { useState } from "react"; const contacts = [ { id: "asha", name: "Asha" }, { id: "ben", name: "Ben" }, { id: "chen", name: "Chen" },]; function ChatWindow({ contact, draft, onDraftChange, onSend }) { return ( <section> <h3>Chat with {contact.name}</h3> <input aria-label="Message" value={draft} onChange={(event) => onDraftChange(event.target.value)} /> <button onClick={onSend}>Send</button> </section> );} export default function App() { const [contact, setContact] = useState(contacts[0]); const [drafts, setDrafts] = useState({}); const draft = drafts[contact.id] ?? ""; function changeDraft(text) { setDrafts({ ...drafts, [contact.id]: text }); } return ( <> {contacts.map((item) => ( <button key={item.id} onClick={() => setContact(item)}> {item.name} {drafts[item.id] ? " (draft)" : ""} </button> ))} <ChatWindow contact={contact} draft={draft} onDraftChange={changeDraft} onSend={() => changeDraft("")} /> </> );}App keeps one draft per contact id, and ChatWindow shows the one it's given. Switching away and back reads Asha's draft from drafts, so the box says Hi Asha.
Check your understanding
For each question, write the tree before and after, place by place, with each component's type and key.
Question 1 of 8Where does React keep a component's state?
Choose an answer to see the explanation.
Question 2 of 8You type Hi in the Gift box, then click Switch. What is in the box?
Read the code, then predictimport { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> ); } export default function App() { const [isOn, setIsOn] = useState(false); return ( <> {isOn ? <NoteBox label="Driver" /> : <NoteBox label="Gift" />} <button onClick={() => setIsOn(!isOn)}>Switch</button> </> ); }Choose an answer to see the explanation.
Question 3 of 8This time the element around the NoteBox changes. You type Hi, then click Switch. What is in the box?
Read the code, then predictimport { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> ); } export default function App() { const [isOn, setIsOn] = useState(false); return ( <> {isOn ? <div><NoteBox label="Gift" /></div> : <article><NoteBox label="Gift" /></article>} <button onClick={() => setIsOn(!isOn)}>Switch</button> </> ); }Choose an answer to see the explanation.
Question 4 of 8Here only the key changes. You type Hi, then click Switch. What is in the box?
Read the code, then predictimport { useState } from "react"; function NoteBox({ label }) { const [text, setText] = useState(""); return ( <p> <label> {label}{" "} <input value={text} onChange={(event) => setText(event.target.value)} /> </label> </p> ); } export default function App() { const [isOn, setIsOn] = useState(false); return ( <> <NoteBox key={isOn ? "on" : "off"} label="Gift" /> <button onClick={() => setIsOn(!isOn)}>Switch</button> </> ); }Choose an answer to see the explanation.
Question 5 of 8Why does a component written inside another component lose its state whenever the outer one renders?
Choose an answer to see the explanation.
Question 6 of 8Two different lists each render a
<ChatWindow key="asha" />. Is that a problem?Choose an answer to see the explanation.
Question 7 of 8Each contact's chat should start empty when you switch. What's the simplest change?
Choose an answer to see the explanation.
Question 8 of 8What happens to a NoteBox's text when
{show && <NoteBox />}gets ashowof false?Choose an answer to see the explanation.
Key takeaways
- React keeps state, not your component function. It ties the state to the component's place in the render tree.
- The same component in the same place keeps its state, whatever props it gets and whichever line of your code made it.
- Removing a component, or putting a different one in its place, throws its state away. A changed parent element takes its whole branch with it.
- Define components at the top level of a file. One written inside another is a new type on every render, so it resets.
- A new
keyresets a component on purpose. Keys only need to be unique among siblings. - To keep state when a component leaves, lift it up, hide the component instead of removing it, or save it outside React.
Remember the one-liner.
Same type, same place, same key: React keeps the state. Change any of them, and it starts fresh.
Coming next: Extracting state logic into a reducer. When a component updates its state in many ways, useReducer gathers every update into one function you can read and test.