You know how to hold and update state. The next skill is deciding what to put in state and in what shape. A good structure makes components short and removes whole kinds of bugs. A bad one leads to code that keeps values in sync by hand, and eventually fails. Five principles cover most cases.
1. Group state that changes together
If two values always change at the same time, merge them into one object rather than keeping two states in step:
import { useState } from "react";
function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
function move() {
setPosition({ x: position.x + 10, y: position.y + 5 });
}
return (
<div>
<button onClick={move}>Move</button>
<p>x = {position.x}, y = {position.y}</p>
</div>
);
}
export default App;Move
x = 10, y = 5Separate values that are changed independently into separate states. A form with many fields is a good place to use one object ({ name, email, city }) with a single update function, as the forms lessons show.
2. Avoid contradictions in state
Suppose you track a message that is being sent with two booleans:
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);Four combinations are possible, but one of them (isSending and isSent both true) makes no sense, and a bug in one handler can produce it. The fix is to use one value that can only be one thing at a time:
import { useState } from "react";
function App() {
const [status, setStatus] = useState("typing"); // "typing" | "sending" | "sent"
function handleSend() {
setStatus("sending");
setTimeout(() => setStatus("sent"), 800);
}
return (
<div>
{status === "typing" && <button className="send" onClick={handleSend}>Send</button>}
{status === "sending" && <p>Sending...</p>}
{status === "sent" && <p>Message sent! <button onClick={() => setStatus("typing")}>Write another</button></p>}
</div>
);
}
export default App;Message sent! Write anotherNow only one state is possible at a time, and each branch of the JSX matches exactly one status. Loading screens follow the same pattern: "idle" | "loading" | "success" | "error" rather than isLoading and hasError and data all at once.
3. Avoid redundant state
If you can calculate a value from props or other state, do not store it. Calculate it while rendering:
import { useState } from "react";
function App() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = (firstName + " " + lastName).trim(); // calculated, not stored
return (
<div>
<input className="first" value={firstName} onChange={e => setFirstName(e.target.value)} placeholder="First name" />
<input className="last" value={lastName} onChange={e => setLastName(e.target.value)} placeholder="Last name" />
<p>Full name: {fullName}</p>
</div>
);
}
export default App;Full name: Asha RaoStoring fullName in a third state would force every handler to remember to update it too, and sooner or later one would forget. With a calculation during render, there is a single source of truth and nothing to keep in sync. The same applies to totals, counts and filtered lists:
import { useState } from "react";
function App() {
const [items] = useState([
{ id: 1, name: "Pen", price: 10, qty: 3 },
{ id: 2, name: "Notebook", price: 45, qty: 1 },
]);
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0); // derived
const count = items.length; // derived
return <p>{count} products, total {total}</p>;
}
export default App;2 products, total 75A related trap is copying props into state: const [name, setName] = useState(props.name) captures the prop only on the first render, and later changes to the prop are ignored. Do this only when you really mean "initial value" (name it initialName).
4. Avoid duplication in state
Storing the same data in two places lets the copies drift apart. A typical case is a list plus the "selected" item:
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]); // a copy of an object that is also in itemsIf a user then edits the item in the list, selectedItem still points to the old object. Store the id of the selected item, and look it up each time:
import { useState } from "react";
function App() {
const [items, setItems] = useState([
{ id: 1, title: "Apples" },
{ id: 2, title: "Bananas" },
]);
const [selectedId, setSelectedId] = useState(1);
const selected = items.find(item => item.id === selectedId); // looked up, not copied
function rename() {
setItems(items.map(item => (item.id === selectedId ? { ...item, title: item.title + "!" } : item)));
}
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>
{item.title} <button className="pick" onClick={() => setSelectedId(item.id)}>Select</button>
</li>
))}
</ul>
<p>Selected: {selected.title}</p>
<button className="rename" onClick={rename}>Rename selected</button>
</div>
);
}
export default App;Apples Select
Bananas! Select
Selected: Bananas!
Rename selectedAfter selecting Bananas and renaming it, "Selected" shows the new title "Bananas!" because it is read from the single list. With a stored copy, it would have kept showing "Bananas".
5. Keep state flat
Deeply nested state is hard to update: every change needs a copy at each level. When data is a tree (comments with replies, a folder structure), consider a flat representation: store items in a list or an object by id, with parent ids or arrays of child ids. Each item can then be updated with one map:
// nested: hard to update a reply three levels down
{ id: 1, text: "A", replies: [{ id: 2, text: "B", replies: [{ id: 3, text: "C", replies: [] }] }] }
// flat: update any item with one map; the tree is built when rendering
[
{ id: 1, text: "A", parentId: null },
{ id: 2, text: "B", parentId: 1 },
{ id: 3, text: "C", parentId: 2 },
]Server data often arrives nested; you can flatten it when you receive it. Not every case needs this, but if you find yourself writing three levels of spread in a handler, it is a signal to rethink the shape.
Where state should live
Besides its shape, think about who owns each piece of state. Put it in the lowest component that needs it, and move it up only when a sibling also needs it (the next lesson). Do not put everything in App "just in case": it makes every change re-render the whole tree and hides which component really needs what.
A checklist for each piece of state
Ask of each state variable:
- Does it change over time? If it never changes, it is a constant or a prop, not state.
- Is it passed in as a prop? Then it is the parent's state, not yours.
- Can I compute it from other state or props? Then do not store it.
- Does it duplicate another state? Store ids rather than copies.
- Can it hold a combination that makes no sense? Merge into one status.
- Is it deeply nested? Flatten it if updates are painful.
Common mistakes
- Storing derived values (full name, totals, filtered lists) in state.
- Using several booleans that can contradict (
isLoading,isError,isDone). - Copying props into state, and then wondering why updates are ignored.
- Keeping a copy of the selected object instead of its id.
- Keeping state in
Appthat only one small component uses. - Mirroring the shape of a server response in state without checking whether it is easy to update.
Exercise
- Replace the three booleans
isLoading,isSuccess,isErrorin an imaginary component with onestatusstate and write the JSX for each status. - Make a product list with quantity buttons, and show the total price as a derived value, not a state.
- Add a "filter" input to a list and show the filtered items by calculating them in render. Check that there is no
filteredItemsstate. - Rewrite a nested comments structure as a flat array with
parentId, and write a function that returns the replies of a comment.
Key points
- Keep the minimum state you need, and calculate everything else from it while rendering.
- Group values that change together, and use a single status value instead of several booleans that can contradict each other.
- Avoid redundant state (a full name stored beside first and last name) and duplicated data (a selected object as well as the list it came from): store ids instead.
- Flat state is easier to update than deeply nested state.