All topics
Patternsintermediate

Immutable Update Patterns for State Management

Conventions for updating nested objects and arrays without mutating the original, central to predictable state management in frameworks like Redux and React.

Updating state without mutating it means producing a new object, copying only the path that changed.

Immutable updates with structural sharing are like renovating a single room in a large house by building a new room and updating just that one doorway's connection in the floor plan, while every other untouched room in the house keeps existing exactly as it physically already was, rather than tearing down and rebuilding the entire house from scratch for one room's renovation.

Key Concepts

1
    // mutation -- React will not re-render
    state.user.name = 'Asha';
2
    // immutable
    const next = { ...state, user: { ...state.user, name: 'Asha' } };
3
Why frameworks require this. React and Redux compare by reference to decide what changed:
4
    prevState === nextState     // if true, nothing re-renders
5
Mutating in place keeps the same reference, so the check says nothing changed and the UI does not update.
6
The array patterns.
    add        [...items, newItem]
    prepend    [newItem, ...items]
    remove     items.filter(i => i.id !== id)
    update     items.map(i => i.id === id ? { ...i, done: true } : i)
    insert     [...items.slice(0, i), item, ...items.slice(i)]
7
And the ones that mutate, which must not be used directly.
    push, pop, shift, unshift, splice, sort, reverse
8
    items.sort()          // mutates
    [...items].sort()     // safe
    items.toSorted()      // safe, newer
9
Nested updates get ugly quickly, which is the honest problem with the pattern:
10
    const next = {
      ...state,
      users: {
        ...state.users,
        [id]: {
          ...state.users[id],
          address: { ...state.users[id].address, city: 'Pune' }
        }
      }
    };
11
Four levels of spread to change one string, and every level is a chance to forget one — which silently keeps the old reference and loses the update.
12
Which is why Immer exists.
    const next = produce(state, draft => {
      draft.users[id].address.city = 'Pune';
    });
13
You write the mutation; it produces a new object, sharing everything unchanged. Redux Toolkit includes it by default for this reason.
14
Flat state avoids the problem entirely.
    { usersById: { 1: {...} }, orderIds: [1, 2, 3] }
15
Normalised state means updates are one or two levels deep. That is the structural fix, and it is better than getting good at deep spreading.
16
The cost to be aware of. Copying the path is cheap; copying a large array on every keystroke is not. Unchanged branches are shared, so the copy is shallow at each level — which is why immutable updates are affordable at all.
17
What the interviewer is probing.1. "Why does immutability matter for change detection?" Probing: the mechanism. Stalls: "It is safer." Moves up: a new reference signals a change, so a cheap identity comparison replaces a deep comparison — which is what memoisation relies on.
18
2. "How do you update a nested property immutably?" Probing: the pattern. Stalls: "Mutate it." Moves up: spread each level along the path and reuse the untouched branches by reference.
19
3. "What breaks if you mutate state directly?" Probing: the symptom. Stalls: "Nothing." Moves up: the reference is unchanged, so memoised components and selectors conclude nothing happened and the UI does not update.
20
4. "What does Immer change?" Probing: the ergonomics. Stalls: "Nothing." Moves up: you write mutating code against a draft and it produces an immutable result, which removes the deep-spread noise.