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