Choosing the State Structure
Good state is state that's hard to get wrong. The aim is to shape it so updates stay simple and the different pieces can't drift out of sync. The guiding line: make your state as simple as it can be, but no simpler.
Principles for structuring state
There are five principles that cover almost every decision here, and the rest of the page is one section per principle. In short: group related state, avoid contradictions, avoid redundant state, avoid duplication, and avoid deeply nested state. Each one exists to remove a way for state to get inconsistent, much like normalizing a database.
Group related state
If two state variables always change together, it's usually cleaner to merge them into one. A dot's x and y move as a pair, so a single object beats two separate variables.
// two variables that always change together
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// better: one object
const [position, setPosition] = useState({ x: 0, y: 0 });
Grouping is also the answer when you don't know how many pieces of state you'll need, like a form where the user can add custom fields. There you keep them in an object or array.
Pitfall. When state is an object, you can't update one field without copying the rest. setPosition({ x: 100 }) would throw away y entirely. Either spread the old fields, setPosition({ ...position, x: 100 }), or keep x and y as two separate variables. See Updating Objects in State.
Avoid contradictions in state
If your state can represent combinations that should be impossible, bugs will eventually find them. A feedback form with two booleans allows isSending and isSent to both be true, which makes no sense.
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
Replace the two booleans with one status that can only hold valid values. Now the impossible combination simply can't exist, and you can still derive the booleans for readability.
const [status, setStatus] = useState("typing"); // 'typing' | 'sending' | 'sent'
const isSending = status === "sending";
const isSent = status === "sent";
Those isSending / isSent constants aren't state, so they can never fall out of sync with each other.
Avoid redundant state
If you can calculate something from props or existing state during render, don't store it in state. A fullName computed from firstName and lastName should not be its own variable, keeping it in sync by hand is pure risk.
// ❌ fullName duplicates what the other two already know
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");
// ✅ just compute it during render
const fullName = firstName + " " + lastName;
Because it's recalculated every render, fullName is always correct the moment either name changes, with no extra setter to remember.
Don't mirror props in state
A common version of redundant state is copying a prop into state. The trap: state is only initialized on the first render, so if the parent later passes a new messageColor, your copy is stale.
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor); // ❌ ignores later prop changes
}
Just use the prop directly. Only copy a prop into state when you intentionally want to ignore future updates, and signal that by naming the prop initialColor or defaultColor.
function Message({ messageColor }) {
const color = messageColor; // ✅ always current
}
function Message({ initialColor }) {
// deliberately keeps only the FIRST value; later changes ignored
const [color, setColor] = useState(initialColor);
}
Avoid duplication in state
When the same data lives in two places, they'll eventually disagree. A menu that stores the whole selectedItem object and the items list duplicates that item, so editing it in the list leaves the selection showing the old copy.
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]); // ❌ a copy of an item
Store the id instead, and look the item up during render. Now there's a single source of truth, so an edit to the list shows up in the selection immediately.
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find((item) => item.id === selectedId);
Avoid deeply nested state
Deeply nested state is painful to update, because changing something deep means copying every object above it. A travel plan modeled as a tree of childPlaces inside childPlaces gets unwieldy fast.
The fix is to flatten (or "normalize") it, the way a database does: keep a flat map from id to object, and let each object store its children's ids rather than the child objects themselves.
{
0: { id: 0, title: "(Root)", childIds: [1, 42, 46] },
1: { id: 1, title: "Earth", childIds: [2, 10, 19] },
2: { id: 2, title: "Africa", childIds: [3, 4, 5] },
3: { id: 3, title: "Botswana", childIds: [] },
}
With this shape, deleting a place only touches two levels, the parent's childIds list and the top-level map, instead of rebuilding a deep chain.
function handleComplete(parentId, childId) {
const parent = plan[parentId];
const nextParent = {
...parent,
childIds: parent.childIds.filter((id) => id !== childId),
};
setPlan({ ...plan, [parentId]: nextParent });
}
Deep dive: improving memory usage. The delete above leaves the removed place (and its whole subtree) sitting in the flat map, just unreferenced. To actually free that memory, recursively remove the node and all its descendants from the map. Immer makes this concise, since you can delete draft[id] while walking the children.
updatePlan((draft) => {
const parent = draft[parentId];
parent.childIds = parent.childIds.filter((id) => id !== childId);
function deleteAllChildren(id) {
draft[id].childIds.forEach(deleteAllChildren);
delete draft[id];
}
deleteAllChildren(childId);
});
One more option: sometimes you can cut nesting by pushing state down into child components. Ephemeral UI state that never needs storing, like whether an item is hovered, can live in the child instead of the parent tree.
Recap
If two state variables always update together, merge them. Choose values that make impossible states impossible, so pieces of state can't contradict each other. Don't keep redundant state you can compute during render, and don't put props into state unless you specifically want to freeze the first value. Avoid duplication: for selection, store an id or index rather than the object itself. And if updating deeply nested state is getting complicated, flatten it into a normalized shape.