Background Image

Solving State update issues in React

Oct
23

A small React state bug I keep seeing: mutating arrays/objects inside a state setter. Here’s the mental model and the quick fix.

React

The Problem

This is one of those React bugs that’s easy to write and weirdly hard to spot: you update state, but the UI doesn’t behave how you expect. Most of the time it comes down to the same root cause — mutating existing arrays/objects instead of returning new ones. Below is the minimal example I ran into, why it happens (batching + referential equality), and the pattern I use to fix it every time.

This behaviour can cause issues when you’re trying to update the state based on the previous state. For example, consider the following code:

const handleFilterChange = (type, value) => {
  setFilters(prevFilters => {
    if (prevFilters[type].includes(value)) {
      prevFilters[type].splice(prevFilters[type].indexOf(value), 1);
    } else {
      prevFilters[type].push(value);
    }
    return prevFilters;
  });
};

In this code, handleFilterChange is a function that updates the filters state based on its previous value. It creates a new filters object, modifies it, and returns it. However, due to React’s batching behaviour, the filters state may not be updated immediately after setFilters is called.

The Solution

The solution to this issue is to ensure that a new object or array is created every time the state is updated. This can be done by using the filter method to remove an item from an array instead of splice, and by using the spread operator (...) to create a new array when adding an item.

Here’s the modified handleFilterChange function:

const handleFilterChange = (type, value) => {
  setFilters(prevFilters => {
	  const newFilters = { ...prevFilters };
	  
	  if (newFilters[type].includes(value)) {
	    newFilters[type] = newFilters[type].filter(item => item !== value);
	  } else {
      newFilters[type] = [...newFilters[type], value];
    }
    
    return newFilters;
  });
};

In this code, filter is used to create a new array that doesn’t include the item to be removed, and the spread operator is used to create a new array that includes the item to be added. This ensures that a new array is created every time, which causes React to update the state immediately.

Conclusion

React’s state update behaviour can be confusing, but understanding it can help you write more effective code. By ensuring that a new object or array is created every time the state is updated, you can avoid issues related to state updates not being applied immediately.

Want to discuss this post?

While I haven't enabled comments to keep things simple and focused, I'm always open to hearing from readers!

If you'd like to discuss anything, feel free to email me at

hello@samdoes.dev

You can also DM me on LinkedIn/X (icons in the header).