Choosing the state structure
Five principles for React state that can't fall out of sync: group it, avoid contradictions, redundancy, and duplication, and keep it flat. Live bugs to fix.
- 01Spot state that can disagreeFind the copy, the stored total, or the pair of booleans that lets a component show two different answers at once.
- 02Apply the five principlesGroup related state, use one status, work values out during render, store ids instead of copies, and flatten nested data.
- 03Keep props out of stateRead a prop directly, and name it
initialSomethingwhen you really want only its first value.
A selected tea that goes out of date
Here is a small order: two teas, each with a quantity. +1 adds one to a tea, and Show puts that tea in the Details line under the list. The code works, mostly. In the result, press +1 next to Green tea:
import { useState } from "react"; const startItems = [ { id: 1, name: "Green tea", qty: 1 }, { id: 2, name: "Mint tea", qty: 1 },]; export default function App() { const [items, setItems] = useState(startItems); const [selectedItem, setSelectedItem] = useState(startItems[0]); function addOne(id) { setItems(items.map((item) => (item.id === id ? { ...item, qty: item.qty + 1 } : item))); } return ( <> <ul> {items.map((item) => ( <li key={item.id}> {item.name} × {item.qty} <button onClick={() => addOne(item.id)}>+1</button>{" "} <button onClick={() => setSelectedItem(item)}>Show</button> </li> ))} </ul> <p>Details: {selectedItem.name} × {selectedItem.qty}</p> </> );}The list says Green tea × 2, and Details still says Green tea × 1. Line 9 keeps the list in state. Line 10 keeps the selected item in a second piece of state, starting with the first item object itself. addOne on lines 12 to 14 makes a new list with a new Green tea object, using map and spread, as state updates should. But selectedItem still points at the old object. Line 26 shows that old object, and nothing tells it that the list moved on.
Nothing here is wrong line by line. The bug is in the structure: the same tea is stored twice, so the two can disagree. Choosing what goes in state, and in what shape, decides which bugs your component can have. Here is the same order with one change: it keeps only the id of the selected tea. Press +1 on Green tea again:
import { useState } from "react"; const startItems = [ { id: 1, name: "Green tea", qty: 1 }, { id: 2, name: "Mint tea", qty: 1 },]; export default function App() { const [items, setItems] = useState(startItems); const [selectedId, setSelectedId] = useState(1); const selectedItem = items.find((item) => item.id === selectedId); function addOne(id) { setItems(items.map((item) => (item.id === id ? { ...item, qty: item.qty + 1 } : item))); } return ( <> <ul> {items.map((item) => ( <li key={item.id}> {item.name} × {item.qty} <button onClick={() => addOne(item.id)}>+1</button>{" "} <button onClick={() => setSelectedId(item.id)}>Show</button> </li> ))} </ul> <p>Details: {selectedItem.name} × {selectedItem.qty}</p> </> );}Details now follows the list. Line 10 keeps selectedId, the number 1. Line 11 finds the item with that id in the current list on every render, with find. Line 23's Show button stores item.id instead of the object. There is one Green tea, in items, and Details reads it.
selectedItem still holds the Green tea object from before +1, so it still says × 1. Right, selectedId holds only 1, and each render looks up item 1 in items: × 2.You could photocopy the page of a café's menu with your favourite tea on it. When the café changes the price, your copy still shows the old one. A bookmark holds no prices at all, only the page. Open the menu at the bookmark, and you always read today's.
- In real life: The café's menu, which it updates
- In React: The items list in state
- In real life: A photocopy of the page with your favourite tea
- In React: selectedItem: a copy of one item
- In real life: The café raises the price; your photocopy shows the old one
- In React: +1 makes a new item; the copy keeps × 1
- In real life: A bookmark on the page instead
- In React: selectedId: just the number 1
- In real life: You open the menu at the bookmark and read today's price
- In React: items.find((item) => item.id === selectedId)
Choose state that can't disagree with itself. Keep each fact in one place, in the simplest shape that holds it, and work out everything else during render. Five principles help: group state that changes together, avoid contradictions, avoid redundant state, avoid duplication, and avoid deep nesting.
Each principle below has a "before" example with a bug its state structure causes, and an "after" example without it. Each row of this table is one principle, the bug it prevents, and the example that shows it:
| Principle | The bug it prevents | The example here |
|---|---|---|
| 1. Group related state | One of two values that belong together gets set, and the other doesn't. | A driver's position, as x and y. |
| 2. Avoid contradictions | Two booleans say things that can't both be true. | An order that is delivered but not shipped. |
| 3. Avoid redundant state | A value you could work out is stored, and goes stale. | A cart total, and a copied price prop. |
| 4. Avoid duplication | The same item is kept twice, and the copies disagree. | The overview's selected tea. |
| 5. Avoid deep nesting | Updates copy many levels, and copies hide inside. | A menu with Green tea in two sections. |
Principle 2: avoid state that contradicts itself
A shop's order page tracks a parcel with two booleans. In the result, press Mark delivered first:
import { useState } from "react"; export default function App() { const [isShipped, setIsShipped] = useState(false); const [isDelivered, setIsDelivered] = useState(false); return ( <> <p>Order 1042: {isShipped ? "shipped" : "not shipped yet"}</p> <p>{isDelivered ? "Delivered to Asha" : "Not delivered yet"}</p> <button onClick={() => setIsShipped(true)}>Mark shipped</button> <button onClick={() => setIsDelivered(true)}>Mark delivered</button> </> );}The page says the order is not shipped yet and Delivered to Asha. Lines 4 and 5 can be set in any order, so the state allows a parcel that arrived before it left. Two booleans make four combinations, and only three are real: packing, shipped, and delivered.
Reacting to input with state showed the fix: one status that names the stage, so the state holds exactly one of the real combinations.
import { useState } from "react"; export default function App() { const [status, setStatus] = useState("packing"); // "packing", "shipped", or "delivered" const isShipped = status === "shipped" || status === "delivered"; const isDelivered = status === "delivered"; return ( <> <p>Order 1042: {isShipped ? "shipped" : "not shipped yet"}</p> <p>{isDelivered ? "Delivered to Asha" : "Not delivered yet"}</p> <button onClick={() => setStatus("shipped")} disabled={status !== "packing"}>Mark shipped</button> <button onClick={() => setStatus("delivered")} disabled={status !== "shipped"}>Mark delivered</button> </> );}Line 4 keeps one status: "packing", "shipped", or "delivered". Lines 5 and 6 still give the JSX its two booleans, but now they're worked out from status on every render, so "delivered" always means "shipped" too. Lines 12 and 13 turn each button on only when its step is next, so Mark delivered waits for Mark shipped.
Principle 3: don't store what you can work out
If you can work a value out from props or other state during render, it shouldn't be state. Stored, it's redundant: a second copy of information you already have, which every handler must keep in step. In this cart, remove Green tea, then press Empty the cart:
import { useState } from "react"; const startItems = [ { id: 1, name: "Green tea", price: 3 }, { id: 2, name: "Lemon cake", price: 4 },]; export default function App() { const [items, setItems] = useState(startItems); const [total, setTotal] = useState(7); function remove(item) { setItems(items.filter((other) => other.id !== item.id)); setTotal(total - item.price); } function emptyCart() { setItems([]); } return ( <> <ul> {items.map((item) => ( <li key={item.id}> {item.name}: ${item.price} <button onClick={() => remove(item)}>Remove</button> </li> ))} </ul> <p>Total: ${total}</p> <button onClick={emptyCart}>Empty the cart</button> </> );}Remove works: line 14 takes the price off the total. But emptyCart on lines 17 to 19 only clears the list, so the empty cart says Total: $4. Line 10 made the total a second piece of state, and one of the two handlers forgot it.
import { useState } from "react"; const startItems = [ { id: 1, name: "Green tea", price: 3 }, { id: 2, name: "Lemon cake", price: 4 },]; export default function App() { const [items, setItems] = useState(startItems); const total = items.reduce((sum, item) => sum + item.price, 0); function remove(item) { setItems(items.filter((other) => other.id !== item.id)); } function emptyCart() { setItems([]); } return ( <> <ul> {items.map((item) => ( <li key={item.id}> {item.name}: ${item.price} <button onClick={() => remove(item)}>Remove</button> </li> ))} </ul> <p>Total: ${total}</p> <button onClick={emptyCart}>Empty the cart</button> </> );}Line 10 is now a plain const that adds up the prices with reduce, on every render. remove lost its second line, and emptyCart can't forget anything. Empty the cart: the total says $0.
items is state. Every render works out the total from it, 3 + 4 = 7, so no handler can forget to update it.A common kind of redundant state is a prop copied into state. Here, PriceTag copies its price prop on line 4. Press Raise the price twice:
import { useState } from "react"; function PriceTag({ price }) { const [shownPrice] = useState(price); return <p>Price tag: ${shownPrice}</p>;} export default function App() { const [price, setPrice] = useState(3); return ( <> <p>Price in the shop: ${price}</p> <PriceTag price={price} /> <button onClick={() => setPrice(price + 1)}>Raise the price</button> </> );}The shop says $5, and the tag still says $3. useState(price) uses its argument only on the first render; after that, React hands back the state it kept, as State showed. The parent passes a new price every time, and the tag ignores it. The fix is to read the prop:
import { useState } from "react"; function PriceTag({ price }) { return <p>Price tag: ${price}</p>;} export default function App() { const [price, setPrice] = useState(3); return ( <> <p>Price in the shop: ${price}</p> <PriceTag price={price} /> <button onClick={() => setPrice(price + 1)}>Raise the price</button> </> );}Line 4 shows price itself, so the tag follows the shop. Copying a prop into state only makes sense when you want to ignore later changes on purpose, like a starting value the user then edits. Then name the prop to say so, with initial or default in front:
function QuantityPicker({ initialQty }) { // Only the first initialQty counts. Later changes to it are ignored on purpose. const [qty, setQty] = useState(initialQty); return <button onClick={() => setQty(qty + 1)}>Quantity: {qty}</button>;}Line 1 takes initialQty, and line 3 uses it only to start the picker's own qty. Anyone reading the name knows that changing it later does nothing. (This component is a fragment: it has no result of its own.)
Principle 4: keep each thing in one place
The order in the overview stored Green tea twice: once in items, and once as selectedItem. That's duplication: the same data in two pieces of state. It looks harmless until one copy changes. Then the two disagree, and the page shows both. Watch it happen. This playground runs both versions from the overview, and prints what their state holds after every render:
selectedItem in state- Green tea × 1
- Mint tea × 1
Details: Green tea × 1
…Press +1 on Green tea, then Show, then +1 again. Then switch to Keep only its id and do the same.
With Keep a copy of the item, press +1 on Green tea: items gets a new Green tea with qty: 2, while selectedItem still holds the old object with qty: 1. The verdict says "Out of date". Press Show on Green tea, and the copy catches up, until the next +1. With Keep only its id, state holds selectedId: 1, which no +1 can make wrong: Details looks up the tea in the newest list each time.
The fix always has the same shape: keep the data once, and keep an id wherever you need to point at it. Then find the item during render. The same goes for "the item being edited", "the open chat", or "the song playing now": store which one, not a copy of it.
Principle 5: keep state flat, not deeply nested
Data often arrives nested: a menu has sections, sections have items. Kept in state that way, two problems appear. Here Green tea is on the menu twice, under Tea and under On sale. Each button raises its tea's price by $1. Press Green tea $3 under Tea:
import { useState } from "react"; const startMenu = { title: "Today's menu", sections: [ { title: "Tea", items: [{ id: 1, name: "Green tea", price: 3 }, { id: 2, name: "Mint tea", price: 3 }] }, { title: "On sale", items: [{ id: 1, name: "Green tea", price: 3 }] }, ],}; export default function App() { const [menu, setMenu] = useState(startMenu); function raisePrice(sectionIndex, itemId) { setMenu({ ...menu, sections: menu.sections.map((section, index) => index !== sectionIndex ? section : { ...section, items: section.items.map((item) => (item.id === itemId ? { ...item, price: item.price + 1 } : item)) }, ), }); } return ( <> <h4>{menu.title}</h4> {menu.sections.map((section, index) => ( <section key={section.title}> <p>{section.title}:</p> {section.items.map((item) => ( <button key={item.id} onClick={() => raisePrice(index, item.id)}> {item.name} ${item.price} </button> ))} </section> ))} </> );}Under Tea, Green tea costs $4. Under On sale, it still costs $3. Lines 6 and 7 hold two separate Green tea objects, so changing one leaves the other: duplication again, hidden inside the nesting. The second problem is raisePrice on lines 14 to 23. To change one price three levels down, it copies the menu, the sections list, the right section, its items list, and the item. Each level needs its own spread or map, and every one is a chance for a mistake.
The fix is to make the state flat: put each tea in one table, looked up by its id, and let the sections list ids. Making data flat like this is often called normalizing it, like a database keeps one table per kind of thing:
import { useState } from "react"; const sections = [ { title: "Tea", teaIds: [1, 2] }, { title: "On sale", teaIds: [1] },]; const startTeas = { 1: { id: 1, name: "Green tea", price: 3 }, 2: { id: 2, name: "Mint tea", price: 3 },}; export default function App() { const [teas, setTeas] = useState(startTeas); function raisePrice(id) { setTeas({ ...teas, [id]: { ...teas[id], price: teas[id].price + 1 } }); } return ( <> <h4>Today's menu</h4> {sections.map((section) => ( <section key={section.title}> <p>{section.title}:</p> {section.teaIds.map((id) => ( <button key={id} onClick={() => raisePrice(id)}> {teas[id].name} ${teas[id].price} </button> ))} </section> ))} </> );}Lines 3 to 6 are the sections, with ids instead of teas. They never change, so they aren't state at all. Lines 8 to 11 keep each tea once, in an object keyed by id. raisePrice on lines 16 to 18 is one line: copy the table, and replace one tea with a copy that costs $1 more. Lines 26 to 30 look each id up in teas. Press Green tea under Tea: both sections say $4, because both read the same tea.
Nesting isn't always wrong. A small object inside state, like the driver's position, is fine. Flatten when updates reach deep, or when the same thing appears in more than one place. If deep updates are hard to avoid, the use-immer library lets you write the change as if you were editing the object, and makes the copies for you; Updating objects in state shows it.
Cleaning up a checkout's state
Real components collect state over time, one feature at a time. Here are the state variables of a checkout page that grew that way. Each one made sense when it was added:
const [items, setItems] = useState(startItems);const [itemCount, setItemCount] = useState(2);const [total, setTotal] = useState(7);const [selectedItem, setSelectedItem] = useState(startItems[0]);const [pinX, setPinX] = useState(2);const [pinY, setPinY] = useState(1);const [isPaying, setIsPaying] = useState(false);const [isPaid, setIsPaid] = useState(false);Eight pieces of state, and almost every handler must keep several of them in step. Run each through the five principles. Each row is one piece of state, the principle it breaks, and what replaces it:
| Before | Principle | After |
|---|---|---|
itemCount | 3. Redundant: items.length says it. | const itemCount = items.length; |
total | 3. Redundant: the prices say it. | const total = items.reduce(…); |
selectedItem | 4. Duplication: a copy of one item. | selectedId in state, and items.find(…). |
pinX and pinY | 1. Group: they always change together. | One pin object. |
isPaying and isPaid | 2. Contradiction: both can be true. | One status. |
items | None: it's the data itself. | Stays as it is. |
The cleaned-up state has four pieces, and three plain values worked out during render:
const [items, setItems] = useState(startItems);const [selectedId, setSelectedId] = useState(1);const [pin, setPin] = useState({ x: 2, y: 1 });const [status, setStatus] = useState("editing"); // "editing", "paying", or "paid" const itemCount = items.length;const total = items.reduce((sum, item) => sum + item.price, 0);const selectedItem = items.find((item) => item.id === selectedId);Lines 1 to 4 are the only state: the items, which item is selected, where the delivery pin is, and one status. Lines 6 to 8 are worked out from them on every render, so they can never disagree with the list. Nothing is stored twice, and no two booleans can clash.
Practise spotting the first four principles. Sort each piece of state by the principle that would fix it:
latandlngfor a map's center, always set togetherwidthandheightof a panel you resize by dragging a cornerisPlayingandisPausedfor a music playerisOpenandisClosedfor a cart draweritemCountkept next toitemsuseState(color), wherecoloris a prop the parent keeps changingselectedProduct, an object copied fromproductseditingComment, a copy of one comment fromcomments
Which of the first four principles would improve each piece of state?
Common mix-ups
- "More state makes a component faster, because it doesn't recalculate." No. Adding up a list or finding an item is quick, and it's always right. Stored copies cost a handler line each, and go wrong when one is missed.
- "Always put everything in one state object." No. Group values that change together. Values that change on their own are simpler as separate state.
- "
useState(prop)keeps the state in step with the prop." No. The argument counts on the first render only. Read the prop directly, or name itinitialSomethingwhen you mean the first value. - "Storing the selected object is fine, because it's the same object." Only until the list changes. Updates make new objects, and the stored one keeps the old values. Store the id.
- "Nested state is wrong." No. A small object in state is fine. Flatten when updates go several levels deep, or when the same data shows up in two places.
- "Booleans are the simplest kind of state." One is. Several that describe one choice are not: they allow combinations the UI can't have. Use one status.
Practice exercises
Each exercise shows its code's result under it, or when you press Edit & run. Find the principle the code breaks first, then use the hints.
You change the last name to Rao. What does the greeting say? Answer first, then press Edit & run and try it.
import { useState } from "react";
export default function App() {
const [firstName, setFirstName] = useState("Asha");
const [lastName, setLastName] = useState("Menon");
const [fullName, setFullName] = useState("Asha Menon");
function handleFirstName(e) {
setFirstName(e.target.value);
setFullName(e.target.value + " " + lastName);
}
return (
<>
<label>
First name <input value={firstName} onChange={handleFirstName} />
</label>
<label>
Last name <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
</label>
<p>Hello, {fullName}</p>
</>
);
}import { useState } from "react"; export default function App() { const [firstName, setFirstName] = useState("Asha"); const [lastName, setLastName] = useState("Menon"); const [fullName, setFullName] = useState("Asha Menon"); function handleFirstName(e) { setFirstName(e.target.value); setFullName(e.target.value + " " + lastName); } return ( <> <label> First name <input value={firstName} onChange={handleFirstName} /> </label> <label> Last name <input value={lastName} onChange={(e) => setLastName(e.target.value)} /> </label> <p>Hello, {fullName}</p> </> );}The last name box calls only setLastName, so fullName keeps "Asha Menon" and the greeting says Hello, Asha Menon. fullName is redundant state, and one handler forgot it.
Fix the greeting by working out the full name instead of storing it. After your fix, change the last name to Rao. What does the greeting say?
import { useState } from "react"; export default function App() { const [firstName, setFirstName] = useState("Asha"); const [lastName, setLastName] = useState("Menon"); const [fullName, setFullName] = useState("Asha Menon"); function handleFirstName(e) { setFirstName(e.target.value); setFullName(e.target.value + " " + lastName); } return ( <> <label> First name <input value={firstName} onChange={handleFirstName} /> </label> <label> Last name <input value={lastName} onChange={(e) => setLastName(e.target.value)} /> </label> <p>Hello, {fullName}</p> </> );}import { useState } from "react"; export default function App() { const [firstName, setFirstName] = useState("Asha"); const [lastName, setLastName] = useState("Menon"); const fullName = firstName + " " + lastName; return ( <> <label> First name <input value={firstName} onChange={(e) => setFirstName(e.target.value)} /> </label> <label> Last name <input value={lastName} onChange={(e) => setLastName(e.target.value)} /> </label> <p>Hello, {fullName}</p> </> );}Line 6 works out fullName on every render, so both boxes update it. The first name's handler goes back to one line, like the last name's.
The Now playing line copies the song. Change the state so it can't go out of date. After your fix, press Play Morning tea, then Like Morning tea twice. What does the Now playing line say?
import { useState } from "react"; const startSongs = [ { id: 1, title: "Morning tea", likes: 0 }, { id: 2, title: "Rainy window", likes: 0 },]; export default function App() { const [songs, setSongs] = useState(startSongs); const [playing, setPlaying] = useState(null); function like(id) { setSongs(songs.map((song) => (song.id === id ? { ...song, likes: song.likes + 1 } : song))); } return ( <> {songs.map((song) => ( <p key={song.id}> {song.title} ({song.likes} likes) <button onClick={() => setPlaying(song)}>Play {song.title}</button>{" "} <button onClick={() => like(song.id)}>Like {song.title}</button> </p> ))} {playing !== null && <p>Now playing: {playing.title} ({playing.likes} likes)</p>} </> );}import { useState } from "react"; const startSongs = [ { id: 1, title: "Morning tea", likes: 0 }, { id: 2, title: "Rainy window", likes: 0 },]; export default function App() { const [songs, setSongs] = useState(startSongs); const [playingId, setPlayingId] = useState(null); const playing = songs.find((song) => song.id === playingId) ?? null; function like(id) { setSongs(songs.map((song) => (song.id === id ? { ...song, likes: song.likes + 1 } : song))); } return ( <> {songs.map((song) => ( <p key={song.id}> {song.title} ({song.likes} likes) <button onClick={() => setPlayingId(song.id)}>Play {song.title}</button>{" "} <button onClick={() => like(song.id)}>Like {song.title}</button> </p> ))} {playing !== null && <p>Now playing: {playing.title} ({playing.likes} likes)</p>} </> );}Line 10 keeps only the id, and line 11 finds the song in the newest list on every render, so the likes always match. ?? null keeps playing as null before you press Play.
Press Bigger, then Reset size. The photo isn't back to its size. Group the width and height. After your fix, what does the page say after Bigger, Bigger, Reset size?
import { useState } from "react"; export default function App() { const [width, setWidth] = useState(200); const [height, setHeight] = useState(150); function bigger() { setWidth(width + 20); setHeight(height + 15); } function resetSize() { setWidth(200); } return ( <> <p>Photo size: {width} × {height}</p> <button onClick={bigger}>Bigger</button> <button onClick={resetSize}>Reset size</button> </> );}import { useState } from "react"; export default function App() { const [size, setSize] = useState({ width: 200, height: 150 }); function bigger() { setSize({ width: size.width + 20, height: size.height + 15 }); } function resetSize() { setSize({ width: 200, height: 150 }); } return ( <> <p>Photo size: {size.width} × {size.height}</p> <button onClick={bigger}>Bigger</button> <button onClick={resetSize}>Reset size</button> </> );}Width and height always change together, so line 4 keeps them as one object. resetSize on line 11 writes a whole size, and can't leave one number behind.
The cart badge never changes. Fix it without adding state to App. After your fix, what does the badge say after two clicks on Add to cart?
import { useState } from "react"; function CartBadge({ count }) { const [shown, setShown] = useState(count); return <p>Cart badge: {shown} in cart</p>;} export default function App() { const [count, setCount] = useState(1); return ( <> <CartBadge count={count} /> <button onClick={() => setCount(count + 1)}>Add to cart</button> </> );}import { useState } from "react"; function CartBadge({ count }) { return <p>Cart badge: {count} in cart</p>;} export default function App() { const [count, setCount] = useState(1); return ( <> <CartBadge count={count} /> <button onClick={() => setCount(count + 1)}>Add to cart</button> </> );}The badge now shows its count prop on every render, so it follows the parent. It needed no state at all: the parent's count is the one place the number lives.
Press Green tea under Tea: it's sold out there, but not under Popular. Restructure the state so each tea is stored once. After your fix, what does Green tea's button under Popular say after you press it under Tea?
import { useState } from "react"; const startSections = [ { title: "Tea", items: [{ id: 1, name: "Green tea", soldOut: false }, { id: 2, name: "Mint tea", soldOut: false }] }, { title: "Popular", items: [{ id: 1, name: "Green tea", soldOut: false }] },]; export default function App() { const [sections, setSections] = useState(startSections); function markSoldOut(sectionIndex, id) { setSections( sections.map((section, index) => index !== sectionIndex ? section : { ...section, items: section.items.map((item) => (item.id === id ? { ...item, soldOut: true } : item)) }, ), ); } return ( <> {sections.map((section, index) => ( <section key={section.title}> <p>{section.title}:</p> {section.items.map((item) => ( <button key={item.id} onClick={() => markSoldOut(index, item.id)}> {item.name} {item.soldOut && " (sold out)"} </button> ))} </section> ))} </> );}import { useState } from "react"; const sections = [ { title: "Tea", teaIds: [1, 2] }, { title: "Popular", teaIds: [1] },]; const startTeas = { 1: { id: 1, name: "Green tea", soldOut: false }, 2: { id: 2, name: "Mint tea", soldOut: false },}; export default function App() { const [teas, setTeas] = useState(startTeas); function markSoldOut(id) { setTeas({ ...teas, [id]: { ...teas[id], soldOut: true } }); } return ( <> {sections.map((section) => ( <section key={section.title}> <p>{section.title}:</p> {section.teaIds.map((id) => ( <button key={id} onClick={() => markSoldOut(id)}> {teas[id].name} {teas[id].soldOut && " (sold out)"} </button> ))} </section> ))} </> );}Each tea lives once, in teas. Both sections look Green tea up by id 1, so marking it sold out under Tea shows it under Popular too. markSoldOut shrank to one line.
Check your understanding
For each question, ask what is stored, and whether anything stored could disagree with something else. Press Edit & run on a question's code to check your answer.
Question 1 of 7Which piece of this state is redundant?
Read the code, then predictJavaScriptconst [items, setItems] = useState(["Tea", "Cake"]); const [count, setCount] = useState(2); const [query, setQuery] = useState("");Choose an answer to see the explanation.
Question 2 of 7After one click on the button, what does the price tag show?
Read the code, then predictimport { useState } from "react"; function PriceTag({ price }) { const [shownPrice] = useState(price); return <p>Price tag: ${shownPrice}</p>; } export default function App() { const [price, setPrice] = useState(3); return ( <> <PriceTag price={price} /> <button onClick={() => setPrice(4)}>Set the price to 4</button> </> ); }Choose an answer to see the explanation.
Question 3 of 7A list page stores
selectedProduct, a copy of one product. What goes wrong when that product's price changes in the list?Choose an answer to see the explanation.
Question 4 of 7An order keeps
isShippedandisDelivered. Which combination should never happen?Choose an answer to see the explanation.
Question 5 of 7When should two values be one state object?
Choose an answer to see the explanation.
Question 6 of 7What does flattening a nested menu into a table of teas by id fix?
Choose an answer to see the explanation.
Question 7 of 7A component takes a prop called
initialQtyand callsuseState(initialQty). What does the name tell you?Choose an answer to see the explanation.
Key takeaways
- Group values that always change together, like a position's x and y, into one object, so no handler can set only half.
- Replace booleans that can contradict each other with one status that names the real combinations.
- Don't store what you can work out from props or state, like a total or a full name. Work it out during render.
- Don't copy a prop into state unless you mean its first value only, and then name it
initialSomething. - Keep each piece of data once. To point at an item, store its id and find it during render.
- Flatten deeply nested data into tables by id, so an update changes one place in one line.
Remember the one-liner.
Store each fact once, in the simplest shape, and work out the rest.
Coming next: Sharing state between components. It applies "keep each thing in one place" across components: when two components need the same state, you move it up to their closest common parent.