Chapter 3 of 4

Objects and Arrays in State

Why you must replace state instead of mutating it, and the patterns for doing so.

React decides whether to re-render by comparing the new state with the old one by reference. Mutating an object keeps the same reference, so React sees no change and skips the update.

// Wrong: same object, React sees nothing new
function addTag(tag) {
  tags.push(tag);
  setTags(tags);
}

// Right: a new array
function addTag(tag) {
  setTags([...tags, tag]);
}

Array patterns

// Add to the end
setItems([...items, newItem]);

// Add to the start
setItems([newItem, ...items]);

// Remove by id
setItems(items.filter((item) => item.id !== id));

// Replace one item
setItems(items.map((item) =>
  item.id === id ? { ...item, done: true } : item
));

// Sort without mutating the original
setItems([...items].sort((a, b) => a.name.localeCompare(b.name)));

Object patterns

const [form, setForm] = useState({ name: "", email: "" });

// Update one field, keep the rest
function handleChange(field, value) {
  setForm({ ...form, [field]: value });
}

// Nested objects need a copy at every level you change
setUser({
  ...user,
  address: { ...user.address, city: "Berlin" },
});

Keep state minimal

Do not store anything you can calculate from something else. Derived values in state go stale; derived values computed during render never do.

// Redundant: total can drift out of sync
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);

// Better: derive it
const [items, setItems] = useState([]);
const total = items.reduce((sum, item) => sum + item.price, 0);