1 octobre 2024
Why Mutating State is Problematic in React
3 minutes de lecture
When I conduct React training sessions, one recurring issue participants face is state mutation. It's often a subtle problem, and to fully grasp it, you need to understand how React manages state.
This article aims to clarify why mutating state is problematic in React and how to effectively avoid it.
The Problem
Let's consider a simple example. You have an array in the state that you want to update:
const [items, setItems] = useState([1, 2, 3])
const addItem = () => {
items.push(4)
setItems(items) // Oops, we're mutating the array here
}
When you run this function, you might expect the component to update with the added item. But no, nothing happens. Why? Because the reference of the items
array hasn't changed. You only modified its content, and React doesn't detect that.
The result: no UI update. What we have done here is a state mutation.
Understanding Mutation
Imagine your application as a residential neighborhood filled with houses, where each variable is a house with its own postal address. When you create a variable, you assign it an address.
Mutating an object is like renovating the interior of a house without changing its address. You can repaint the walls, add furniture, but the address stays the same.
React, on the other hand, doesn't look inside the houses. It only cares whether the address has changed. If the address remains the same, React assumes there is no change and doesn't trigger an update. This is where mutating state becomes problematic: you're altering the inside of the object, but React has no idea and continues to rely on the old version of the UI.
Undesirable Effects
The problem can also occur in hooks like useEffect
, where mutating the state can prevent side effects from running. A classic example:
user.username = 'Romy'
useEffect(() => {
console.log('user has changed!')
}, [user])
If you mutate the user
object instead of creating a new reference, the effect will never be called. This bug doesn't generate visible errors but results in unexpected behaviors that can accumulate.
The Solution: Copy, Don't Mutate
So how do we avoid this problem? The solution is simple: instead of mutating your objects or arrays, create new copies using operators like the spread operator (...
) or methods like concat
. Here's how to fix our previous example:
const addItem = () => {
const newItems = [...items, 4] // New reference
setItems(newItems) // State update
}
Here, we're creating a new array, which generates a new reference. React detects this change, and the UI updates correctly.
Similarly, for objects:
const updateUser = () => {
const newUser = { ...user, age: 30 } // New reference
setUser(newUser)
}
Note that for simple variables known as atomic (string, number, boolean...), we don't have this mutation problem.
These data types are immutable by nature in JavaScript. When you "modify" a variable of this type, you're actually creating a new value with a new reference. This is why React can easily detect changes on these data types.
Why This Matters
React relies on reference comparison to optimize UI updates. Instead of performing a deep comparison (which would be performance-intensive), it simply checks if a new reference has been created. If it has, React knows it needs to reconcile the DOM with the new state.
This optimization is crucial for maintaining acceptable performance, especially in complex applications. Comparing every property of an object or every element of an array on every state change would significantly slow down rendering. By creating a new reference, you ensure a smooth and predictable update process.
In Summary
Mutating state is like redecorating a house without changing the address: no one notices the changes. Creating a new reference, however, allows React to do its job and update the UI.
So, next time you're managing state, remember: don't mutate, duplicate!
👋🏼