State Immutability

Difficulty: Beginner

Immutability means never modifying existing data directly. Instead, you create a new copy with the changes applied. In React, this is not just a best practice - it is a requirement. React determines whether to re-render a component by comparing the previous state reference with the new state reference. If you mutate an object in place, the reference stays the same, and React will not detect the change. Your UI will be out of sync with your data.

When updating objects in state, you must create a new object with the spread operator and override the properties that changed. For example, if state is `{ name: 'Alice', age: 25 }`, updating the age looks like `setState({ ...prev, age: 26 })`. The spread copies all existing properties, and the `age: 26` at the end overrides the old value. You should never do `state.age = 26` - this mutates the existing object.

Updating nested objects requires spreading at every level of nesting. If state is `{ user: { name: 'Alice', address: { city: 'NYC' } } }`, updating the city requires: `setState({ ...prev, user: { ...prev.user, address: { ...prev.user.address, city: 'LA' } } })`. This can get verbose with deeply nested structures, which is why libraries like Immer exist to simplify immutable updates.

Arrays in state must also be updated immutably. Instead of `push`, use spread or `concat` to add items: `[...prev, newItem]`. Instead of `splice`, use `filter` to remove items: `prev.filter(item => item.id !== removeId)`. Instead of direct index assignment, use `map` to update items: `prev.map(item => item.id === updateId ? { ...item, done: true } : item)`. These operations all return new arrays without modifying the original.

Understanding shallow vs deep copying is important. The spread operator creates a shallow copy - it copies the top-level properties, but nested objects still reference the same memory. This is usually fine for React because you spread at each level of nesting that you modify. Deep cloning (e.g., `structuredClone`) is rarely needed and can be expensive. The key rule is: create new references for every object you modify in the path from root to the changed value.

Code examples

Immutable Object Updates

// WRONG: Mutating state directly
const user = { name: 'Alice', age: 25, role: 'developer' };

// Don't do this:
// user.age = 26;  // Mutation!

// RIGHT: Create a new object with spread
const updatedUser = { ...user, age: 26 };

console.log('Original age: ' + user.age);
console.log('Updated age: ' + updatedUser.age);
console.log('Same object? ' + (user === updatedUser));
console.log('Name preserved: ' + updatedUser.name);

The spread operator creates a new object with all properties copied. Overriding a property after the spread updates only that property. The original object is untouched.

Immutable Array Updates

const todos = [
  { id: 1, text: 'Learn React', done: false },
  { id: 2, text: 'Build app', done: false }
];

// ADD: spread existing + new item
const added = [...todos, { id: 3, text: 'Deploy', done: false }];
console.log('After add: ' + added.length + ' items');

// REMOVE: filter out the item
const removed = todos.filter(t => t.id !== 1);
console.log('After remove: ' + removed.map(t => t.text).join(', '));

// UPDATE: map and replace the matching item
const toggled = todos.map(t =>
  t.id === 1 ? { ...t, done: true } : t
);
console.log('After toggle: ' + toggled[0].text + ' done=' + toggled[0].done);
console.log('Original unchanged: ' + todos[0].done);

Use spread to add, filter to remove, and map to update items in arrays. Each operation returns a new array without modifying the original.

Updating Nested State

const state = {
  user: {
    name: 'Alice',
    preferences: {
      theme: 'light',
      language: 'en'
    }
  }
};

// Update deeply nested property immutably
const newState = {
  ...state,
  user: {
    ...state.user,
    preferences: {
      ...state.user.preferences,
      theme: 'dark'
    }
  }
};

console.log('Old theme: ' + state.user.preferences.theme);
console.log('New theme: ' + newState.user.preferences.theme);
console.log('Language preserved: ' + newState.user.preferences.language);
console.log('Name preserved: ' + newState.user.name);

For nested updates, spread at every level in the path to the changed value. This creates new references only for the modified branch of the object tree.

Key points

Concepts covered

immutability, updating objects in state, updating arrays in state, spread operator, shallow copy