Updated June 2026. Tested on React 19 and modern JavaScript. Part of the Techalyst React series.

One rule causes more "but I changed the state, why didn't the UI update" bugs than any other in React: never mutate state, always replace it. It sounds like a style preference until you know the reason, after which it becomes second nature. React decides whether to re-render by comparing references, so if you edit an object in place, it is still the same reference, and React can miss the change entirely.

Why mutation breaks React

When you call a setter, React compares the new value to the old one by reference. If you mutate the existing object and pass it back, the reference is identical, so React may conclude nothing changed and skip the re-render:

// Wrong: same array reference, React may not re-render
todos.push(newTodo);
setTodos(todos);

Even when it does re-render, you have broken the model that state is a stable snapshot per render. The fix is always to hand the setter a brand new object or array.

Updating objects

Copy the object with the spread operator and override the field you are changing:

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

The spread makes a fresh object with all the old fields, then name overwrites the one you want. New reference, React re-renders, the rest of the fields carry over untouched.

Updating arrays

Arrays are where this comes up most, and there are three moves you will use constantly. The trick is to use the methods that return a new array, not the ones that mutate in place:

// Add
setItems([...items, newItem]);

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

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

filter and map return new arrays and leave the original alone, which is exactly what you want. Avoid push, splice, sort and reverse on state, they mutate in place. If you need a sorted copy, spread first: [...items].sort(...).

Nested state

Deeply nested state is the awkward case, because immutability has to go all the way down, you copy at each level you change:

setState({
  ...state,
  user: {
    ...state.user,
    address: { ...state.user.address, city: 'Colombo' },
  },
});

That gets verbose fast. Two ways out: keep your state shallow in the first place, which is usually the better design, or use a helper like Immer that lets you write what looks like a mutation and produces an immutable update under the hood. Reach for Immer when the state genuinely has to be deep, not as a way to avoid learning the spread.

Wrapping up

React updates the screen by comparing references, so mutating state in place can leave the UI stale. Always create a new value: spread an object and override the changed field, and use map, filter and the spread to add, remove and update array items rather than push or splice. Keep state shallow where you can so the copies stay simple, and reach for Immer only when nesting is unavoidable. This one habit, replace, never mutate, removes a whole category of confusing bugs and is the foundation everything from useState to useReducer relies on.