| |

React 18 ⚛️ Updating Objects in State

Objects in React state are updated by replacement, not by mutation. An object held in state is treated as immutable: when a change is needed, a new object is created with the changed property and the remaining properties copied from the old one. React compares the old and new references to detect the change, and a mutation in place does not change the reference, so the update is not detected. This is the rule that makes object state work, and violating it is the most common source of bugs that appear when state updates are not reflected in the UI.

The pattern for updating an object is the spread operator. { ...user, name: 'Alice' } creates a new object with all the properties of user, then overrides name. The original object is untouched, and the new object has a new reference. React sees the new reference and re-renders. For nested objects, the spread is applied at each level that changes: { ...user, address: { ...user.address, city: 'Oslo' } }. The levels that do not change keep their references, which allows React and memoized components to skip work they do not need to do.

This chapter covers the immutability rule, the spread pattern, updating a single property, updating multiple properties, nested objects, computed property names, arrays of objects, and the mistakes that break the update.

Key point: Never mutate an object in state. Create a new object with the spread operator, overriding the properties that change. For nested objects, spread at each level that changes. Use the functional form of the setter when the new object depends on the previous one: setUser(user => ({ ...user, name: 'Alice' })). The parentheses around the object are required because the arrow function would otherwise interpret the braces as a block.


Why object state is updated by replacement

The reference comparison problem. React detects state changes by comparing the new state to the old one with Object.is. If the reference is the same, React skips the re-render. Mutating an object in place and passing it to the setter leaves the reference unchanged, so React does not detect the change and does not re-render.

The immutability problem. An object in state may be referenced by other parts of the component tree. If one part mutates it, other parts see the change without re-rendering, which produces inconsistent UI. Immutability prevents this by ensuring that every change produces a new object and the old one remains valid.

The memoization problem. React.memo and useMemo compare props and dependencies by reference. If an object is mutated in place, the reference does not change, and the memoized computation is not re-run. Spreading produces a new reference, which triggers the re-computation when the content changes.

The undo problem. If the state holds a history of objects, mutating in place destroys the previous versions. Creating new objects preserves the history, which makes undo and time-travel debugging possible.

The correctness problem. The spread pattern is straightforward once it is understood. The mistakes come from forgetting it: mutating the object, spreading the wrong level, or forgetting the parentheses in the functional form. Each mistake produces a state update that does not work.


a. The immutability rule

An object in state is not modified. When a property changes, a new object is created with the new value and the old values.

const [user, setUser] = useState({ name: 'Alice', age: 30 });

// Wrong: mutation
user.name = 'Bob';
setUser(user);  // same reference; React does not re-render

// Right: new object
setUser({ ...user, name: 'Bob' });  // new reference; React re-renders

The wrong version mutates the object and passes the same reference to the setter. React compares the new state to the old with Object.is, sees the same reference, and skips the re-render. The UI does not update.

The right version creates a new object with the spread operator. The name property is overridden, and the other properties are copied. The reference is new, so React detects the change and re-renders.


b. Updating a single property

The spread operator copies the existing properties, and the new property overrides the one that changes.

const [user, setUser] = useState({ name: 'Alice', age: 30, city: 'Oslo' });

// Update the name
setUser({ ...user, name: 'Bob' });

// Result: { name: 'Bob', age: 30, city: 'Oslo' }

The order matters. The spread comes first, and the override comes after. If the override comes first, the spread overwrites it.

// Wrong: the spread overwrites the override
setUser({ name: 'Bob', ...user });
// Result: name is 'Alice' (the original value)

The convention is to place the spread first and the overrides after, so the overrides win.


c. Updating multiple properties

Multiple properties are overridden in the same object literal.

setUser({ ...user, name: 'Bob', age: 31 });

The new object has the updated name and age, and the other properties are copied from the old object.

The same pattern works with a computed property name:

const field = 'name';
setUser({ ...user, [field]: 'Bob' });

The square brackets evaluate the expression and use its result as the property name. This is the pattern for updating a field in a form where the field name is stored in a variable.

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

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
    </form>
  );
}

One handler updates both fields. The name attribute of the input determines which property is updated. This is the standard pattern for forms with multiple fields.


d. Nested objects

Updating a nested property requires spreading at each level that changes.

const [user, setUser] = useState({
  name: 'Alice',
  address: { city: 'Oslo', country: 'Norway' },
});

// Wrong: mutation of the nested object
user.address.city = 'Bergen';

// Wrong: spread only at the top level
setUser({ ...user, address: { city: 'Bergen' } });
// This drops the country property

// Right: spread at each level
setUser({
  ...user,
  address: { ...user.address, city: 'Bergen' },
});

The right version creates a new object at the top level and a new object at the address level. The country property is copied from the old address, so it is preserved.

The rule is: spread at every level from the changed property up to the root. If the change is at the third level, spread three times.

setUser({
  ...user,
  address: {
    ...user.address,
    coordinates: {
      ...user.address.coordinates,
      lat: 60.39,
    },
  },
});

The deeper the object, the more spreads are needed. If the depth is consistently large, the state shape may be too nested, and flattening it is worth considering.


e. Arrays of objects

An array in state is updated by creating a new array. When the array holds objects, the object that changes is also replaced.

const [items, setItems] = useState([
  { id: 1, name: 'A', done: false },
  { id: 2, name: 'B', done: false },
]);

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

The map creates a new array. For the item that changes, a new object is created with the spread. For the other items, the same object is used, so their references do not change. React and memoized components can skip work on the unchanged items.

Add an item.

setItems((items) => [...items, newItem]);

Remove an item.

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

Update an item.

setItems((items) =>
  items.map((item) => (item.id === id ? { ...item, ...changes } : item))
);

Each operation creates a new array, and the update creates a new object for the changed item.


f. The functional form and the parentheses

When the new object depends on the previous object, use the functional form of the setter. The function receives the previous state and returns the new one.

setUser((prev) => ({ ...prev, name: 'Bob' }));

The parentheses around the object are required. Without them, JavaScript interprets the braces as the start of a function body, not an object literal.

// Wrong: the braces are a function body
setUser((prev) => { ...prev, name: 'Bob' });
// This is a syntax error or an unintended block

// Right: parentheses around the object
setUser((prev) => ({ ...prev, name: 'Bob' }));

The functional form is the correct choice when the update depends on the previous value, which is almost always the case for object updates. It avoids the stale closure problem and works correctly when the setter is called multiple times or from an asynchronous callback.


Complete Example Session

// ============================================
// PART 1: WRONG — MUTATION
// ============================================
function Profile() {
  const [user, setUser] = useState({ name: 'Alice', age: 30 });

  const updateName = () => {
    user.name = 'Bob';
    setUser(user);  // same reference; no re-render
  };

  return <button onClick={updateName}>{user.name}</button>;
}
// ============================================
// PART 2: RIGHT — NEW OBJECT
// ============================================
function Profile() {
  const [user, setUser] = useState({ name: 'Alice', age: 30 });

  const updateName = () => {
    setUser({ ...user, name: 'Bob' });
  };

  return <button onClick={updateName}>{user.name}</button>;
}
// ============================================
// PART 3: FUNCTIONAL FORM
// ============================================
function Profile() {
  const [user, setUser] = useState({ name: 'Alice', age: 30 });

  const updateName = () => {
    setUser((prev) => ({ ...prev, name: 'Bob' }));
  };

  return <button onClick={updateName}>{user.name}</button>;
}
// ============================================
// PART 4: MULTIPLE PROPERTIES
// ============================================
setUser({ ...user, name: 'Bob', age: 31 });
// ============================================
// PART 5: COMPUTED PROPERTY NAME
// ============================================
const field = 'name';
setUser({ ...user, [field]: 'Bob' });
// ============================================
// PART 6: FORM WITH ONE HANDLER
// ============================================
function Form() {
  const [form, setForm] = useState({ name: '', email: '' });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
    </form>
  );
}
// ============================================
// PART 7: NESTED OBJECT — WRONG
// ============================================
setUser({ ...user, address: { city: 'Bergen' } });
// Drops the country property
// ============================================
// PART 8: NESTED OBJECT — RIGHT
// ============================================
setUser({
  ...user,
  address: { ...user.address, city: 'Bergen' },
});
// ============================================
// PART 9: ARRAY OF OBJECTS — UPDATE
// ============================================
setItems((items) =>
  items.map((item) =>
    item.id === id ? { ...item, done: true } : item
  )
);
// ============================================
// PART 10: DEEP NESTED
// ============================================
setUser({
  ...user,
  address: {
    ...user.address,
    coordinates: {
      ...user.address.coordinates,
      lat: 60.39,
    },
  },
});

These ten parts cover the wrong mutation, the right new object, the functional form, multiple properties, computed property names, a form with one handler, a nested object done wrong, a nested object done right, an array of objects, and a deeply nested update.


Quick Reference

Object Update Patterns

OperationPattern
Update one property{ ...obj, key: value }
Update many{ ...obj, a: 1, b: 2 }
Computed key{ ...obj, [key]: value }
FunctionalsetObj((prev) => ({ ...prev, key: value }))
Nested{ ...obj, nested: { ...obj.nested, key: value } }

Array of Objects

OperationPattern
Add[...items, item]
Removeitems.filter((i) => i.id !== id)
Updateitems.map((i) => i.id === id ? { ...i, ...changes } : i)

Wrong vs Right

WrongRight
obj.key = value{ ...obj, key: value }
setObj(obj)setObj({ ...obj, key: value })
{ key: value, ...obj }{ ...obj, key: value }
setObj((prev) => { ...prev })setObj((prev) => ({ ...prev }))

Nested Spread

LevelSpreads needed
Top1
Second2
Third3
Fourth4

Best Practices

✅ Do This:

// Spread to create a new object
setUser({ ...user, name: 'Bob' });

// Place the spread first, overrides after
setUser({ ...user, name: 'Bob', age: 31 });

// Use the functional form when depending on the previous value
setUser((prev) => ({ ...prev, name: 'Bob' }));

// Spread at each level that changes
setUser({ ...user, address: { ...user.address, city: 'Bergen' } });

// Use a computed property name for dynamic fields
setForm((prev) => ({ ...prev, [name]: value }));

❌ Don’t Do This:

// Mutate the object
user.name = 'Bob';  // ❌
setUser(user);

// Spread after the override
setUser({ name: 'Bob', ...user });  // ❌

// Forget the nested spread
setUser({ ...user, address: { city: 'Bergen' } });  // ❌ drops properties

// Omit the parentheses in the functional form
setUser((prev) => { ...prev, name: 'Bob' });  // ❌ syntax error

// Mutate the previous state inside the function
setUser((prev) => { prev.name = 'Bob'; return prev; });  // ❌

Common Pitfalls

PitfallWhy It HappensFix
UI does not updateMutated the objectUse the spread
Properties lostSpread only at the top levelSpread at each level
Override ignoredSpread after the overrideSpread first
Syntax errorMissing parentheses(prev) => ({ ...prev })
Stale valueUsed the direct formUse the functional form
Unchanged items re-renderNew object for every itemOnly create a new object for the changed item

Real-World Examples

1. Update Name

setUser({ ...user, name: 'Bob' });

2. Update Multiple

setUser({ ...user, name: 'Bob', age: 31 });

3. Functional Update

setUser((prev) => ({ ...prev, name: 'Bob' }));

4. Form Field

setForm((prev) => ({ ...prev, [name]: value }));

5. Nested City

setUser({ ...user, address: { ...user.address, city: 'Bergen' } });

6. Add to Array

setItems((items) => [...items, newItem]);

7. Remove from Array

setItems((items) => items.filter((i) => i.id !== id));

8. Update in Array

setItems((items) => items.map((i) =>
  i.id === id ? { ...i, done: true } : i));

9. Deep Nested

setUser({
  ...user,
  address: {
    ...user.address,
    coordinates: { ...user.address.coordinates, lat: 60.39 },
  },
});

10. Reset Object

setUser({ name: '', age: 0, city: '' });

Visual

Mutation vs Replacement

┌──────────────────────────────────────────────────────────────┐
│  MUTATION:                                                   │
│  user.name = 'Bob';                                          │
│  ┌───────────────┐                                           │
│  │ { name: 'Bob',│  ← same object, same reference            │
│  │   age: 30 }   │                                           │
│  └───────────────┘                                           │
│  setUser(user) → React sees the same reference → no re-render│
│                                                              │
│  REPLACEMENT:                                                │
│  setUser({ ...user, name: 'Bob' });                          │
│  ┌───────────────┐  ┌───────────────┐                        │
│  │ { name: 'Alice',│ │ { name: 'Bob',│  ← new object          │
│  │   age: 30 }   │  │   age: 30 }   │    new reference       │
│  └───────────────┘  └───────────────┘                        │
│  React sees a new reference → re-render                      │
└──────────────────────────────────────────────────────────────┘

Spread Order

┌──────────────────────────────────────────────────────────────┐
│  CORRECT:                                                    │
│  { ...user, name: 'Bob' }                                    │
│  └── Spread first, override after → name is 'Bob'            │
│                                                              │
│  WRONG:                                                      │
│  { name: 'Bob', ...user }                                    │
│  └── Override first, spread after → name is the original     │
└──────────────────────────────────────────────────────────────┘

Nested Spread

┌──────────────────────────────────────────────────────────────┐
│  user                                                        │
│  ├── name: 'Alice'                                           │
│  └── address                                                 │
│      ├── city: 'Oslo'        ← change this                   │
│      └── country: 'Norway'                                   │
│                                                              │
│  setUser({                                                   │
│    ...user,                  ← spread level 1                │
│    address: {                                                │
│      ...user.address,        ← spread level 2                │
│      city: 'Bergen',         ← override                      │
│    },                                                        │
│  });                                                         │
└──────────────────────────────────────────────────────────────┘

Parentheses in the Functional Form

┌──────────────────────────────────────────────────────────────┐
│  WRONG:                                                      │
│  setUser((prev) => { ...prev, name: 'Bob' });                │
│  └── Braces are a function body, not an object               │
│                                                              │
│  RIGHT:                                                      │
│  setUser((prev) => ({ ...prev, name: 'Bob' }));              │
│  └── Parentheses make the braces an object literal           │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
RuleNever mutate object state
UpdateCreate a new object with spread
Pattern{ ...obj, key: value }
OrderSpread first, override after
FunctionalsetObj((prev) => ({ ...prev, key: value }))
ParenthesesRequired around the object
NestedSpread at each level
Array of objectsmap with spread for the changed item
MemoizationNew reference triggers re-computation
Unchanged itemsKeep the same reference

Key takeaways:

  • Object state is updated by replacement, not mutation. Create a new object with the spread operator, override the properties that change, and pass it to the setter. React compares references to detect the change.
  • Mutating the object does not trigger a re-render. The reference stays the same, React’s comparison fails, and the UI does not update. This is the most common bug in object state.
  • Place the spread first and the overrides after. The order determines which value wins. The spread copies the old values, and the overrides replace the ones that change.
  • Nested objects require spreading at each level. Spread at every level from the changed property up to the root. Missing a level drops the properties at that level.
  • Use the functional form when the new object depends on the previous one. The function receives the previous state and returns the new object. The parentheses around the object are required.
  • Arrays of objects use map with spread for the changed item. The changed item gets a new object, and the unchanged items keep their references. This lets memoized components skip work on the items that did not change.
  • Computed property names handle dynamic fields. { ...prev, [name]: value } updates the property whose name is in the name variable. This is the pattern for forms with multiple fields and a single handler.

Remember: Objects in React state are immutable. The update is a replacement, not a modification. The spread operator creates the new object, the overrides change the properties, and the new reference tells React to re-render. The mistakes come from forgetting the rule: mutating in place, spreading only the top level, placing the spread after the override, or omitting the parentheses in the functional form. Each mistake produces an update that does not work or a state that loses data. The pattern is simple once it is understood, and it applies to objects, nested objects, and arrays of objects alike.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!