Course outline · 0% complete

0/28 lessons0%

Course overview →

Objects and arrays in state

lesson 4-3 · ~15 min · 12/28

Objects and arrays in state

Real state is rarely a lone number, it is a list of tasks, a form object, a cart. Updating those wrongly is the single most common React bug in code review: the data changes but the screen does not. This lesson is about why that happens and the update patterns that prevent it.

React decides whether state changed by comparing the old and new values with Object.is, essentially ===. For numbers and strings that is simple. For objects and arrays, === compares identity, is it the same object in memory, not the contents.

So if you mutate an object and set it back, React sees the same object and may skip the re-render:

const [user, setUser] = useState({ name: "Amara", plan: "free" });

// WRONG: mutates the existing object, identity unchanged
user.plan = "pro";
setUser(user);

// RIGHT: build a NEW object with the change
setUser({ ...user, plan: "pro" });

The spread syntax { ...user, plan: "pro" } from Advanced JavaScript copies every field of user into a fresh object, then overrides plan. New object, new identity, guaranteed re-render.

Code exercise · javascript

See identity vs contents with your own eyes. The first comparison is true because mutation keeps the same object. The second is false because spread built a new one, which is what React needs.

Spread is shallow: nested objects need a spread per level

One trap remains. Spread copies only the top level of an object. Field values that are themselves objects are copied by reference, the new object and the old one share them. So spreading once and then mutating a nested object still mutates shared data:

const [user, setUser] = useState({
  name: "Amara",
  address: { city: "Lagos", zip: "100001" },
});

// WRONG: copies user, but address is still the SAME shared object
const next = { ...user };
next.address.city = "Accra";   // also changes user.address.city!

// RIGHT: a new object at every level you change
setUser({ ...user, address: { ...user.address, city: "Accra" } });

The rule: to change something nested, spread every object on the path from the root to the field, and change only the field. Deeply nested state gets tedious, which is one reason experienced React developers keep state shallow.

Code exercise · javascript

Your turn. Build moved from user without mutating anything: same user but with address.city set to "Accra". You need a spread at both levels. The last two lines prove both the root object and the nested address are new.

Arrays: no push, no splice

The same rule applies to arrays in state. Mutating methods like push, splice, and sort change the array in place, so React may not notice. Use the non-mutating tools from Advanced JavaScript instead:

You want toAvoidUse
add an itempush[...tasks, newTask]
remove an itemsplicetasks.filter(t => t.id !== id)
change an itemtasks[i] = xtasks.map(t => t.id === id ? {...t, done: true} : t)

Every update produces a new array, leaving the old one untouched.

Code exercise · javascript

Your turn. Without mutating tasks: build added by appending "call mom" with spread, then build removed by filtering "pay rent" out of added. The final line proves the original array survived untouched.

Quiz

Spot the bug: ```jsx const [items, setItems] = useState([]); function addItem(item) { items.push(item); setItems(items); } ```