5 min read
0%

Immutability and Structural Sharing

Back to Blog
Immutability and Structural Sharing

Immutability and Structural Sharing

React relies on reference equality to detect changes. Mutating state directly breaks this — React can’t see the change and skips the re-render. Structural sharing is how you update immutably without copying the entire state tree.

Why Immutability

// Bug: mutating state directly
const [items, setItems] = useState([{ id: 1, name: 'Apple' }]);

function updateItem(name) {
  items[0].name = name; // mutates the existing array
  setItems(items);      // same reference — React bails out, no re-render
}

React compares prevState === nextState. Same reference → equal → no render.

Correct Update Patterns

Create new references for changed parts only:

// Update a field in an object
setUser(prev => ({ ...prev, name: 'Alice' }));

// Add to an array
setItems(prev => [...prev, newItem]);

// Remove from an array
setItems(prev => prev.filter(item => item.id !== idToRemove));

// Update one item in an array
setItems(prev => prev.map(item =>
  item.id === targetId ? { ...item, name: 'Updated' } : item
));

Structural Sharing

You don’t copy everything — only the parts that changed. Unchanged subtrees keep their original references.

const state = {
  users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
  settings: { theme: 'dark' },
};

// Update only user 1's name
const nextState = {
  ...state,                    // settings keeps the same reference
  users: state.users.map(u =>
    u.id === 1 ? { ...u, name: 'Alicia' } : u  // user 2 keeps same reference
  ),
};

nextState.settings === state.settings; // true — reused
nextState.users[1] === state.users[1]; // true — reused

Unchanged references allow React.memo and useMemo to skip re-renders for those subtrees.

Deep Nesting

Immutable updates on deeply nested state are verbose:

const next = {
  ...state,
  a: {
    ...state.a,
    b: {
      ...state.a.b,
      value: 'new',
    },
  },
};

Use Immer to write mutable-looking code that produces immutable results:

import produce from 'immer';

const next = produce(state, draft => {
  draft.a.b.value = 'new'; // Immer handles the copying
});

useImmer integrates this with useState directly.


Canvas is not supported in your browser