Everybody says don’t do it. Never mutate state directly, always call setState
.
But why, though?
If you’ve tried it out, you might’ve noticed nothing bad happened. If you modify state directy, call this.setState({})
or even this.forceUpdate()
, then everything might appear to be just fine.
this.state.cart.push(item.id);
this.setState({ cart: this.state.cart });
// renders like normal! maybe?
This is a bad idea for two reasons (even though it would work in this example, and many others).
(other patterns to avoid are things like this.state.something = x
and this.state = x
)
Mutating state directly can lead to odd bugs, and components that are hard to optimize. Here’s an example.
As you may already know, a common way to tune a React component for performance is to make it “pure,” which causes it to only re-render when its props change (instead of every time its parent re-renders). This can be done automatically by extending React.PureComponent
instead of React.Component
, or manually by implementing the shouldComponentUpdate
lifecycle method to compare nextProps
with current props. If the props look the same, it skips the render, and saves some time.
Here is a simple component that renders a list of items (notice that it extends React.PureComponent
):
class ItemList extends React.PureComponent {
render() {
return (
<ul>
{this.props.items.map(item => <li key={item.id}>{item.value}</li>)}
</ul>
);
}
}
Now, here is a tiny app that renders the ItemList
and allows you to add items to the list – the good way (immutably), and the bad way (by mutating state). Watch what happens.
class App extends Component {
// Initialize items to an empty array
state = {
items: []
};
// Initialize a counter that will increment
// for each item ID
nextItemId = 0;
makeItem() {
// Create a new ID and use
// a random number as the value
return {
id: this.nextItemId++,
value: Math.random()
};
}
// The Right Way:
// copy the existing items and add a new one
addItemImmutably = () => {
this.setState({
items: [...this.state.items, this.makeItem()]
});
};
// The Wrong Way:
// mutate items and set it back
addItemMutably = () => {
this.state.items.push(this.makeItem());
this.setState({ items: this.state.items });
};
render() {
return (
<div>
<button onClick={this.addItemImmutably}>
Add item immutably (good)
</button>
<button onClick={this.addItemMutably}>Add item mutably (bad)</button>
<ItemList items={this.state.items} />
</div>
);
}
}
Try it out!
Click the immutable Add button a few times and notice how the list updates as expected.
Then click the mutable Add button and notice how the new items don’t appear, even though state is being changed.
Finally, click the immutable Add button again, and watch how the ItemList re-renders with all the missing (mutably-added) items.
This happens because ItemList
is pure, and because pushing a new item on the this.state.items
array does not replace the underlying array. When ItemList
is asked to re-render, it will notice that its props haven’t changed and it will not re-render.
Recap
So there you go: that’s why you shouldn’t mutate state, even if you immediately call setState. Optimized components might not re-render if you do, and the rendering bugs will be tricky to track down.
Instead, always create new objects and arrays when you call setState
, which is what we did above with the spread operator. Learn more about how to use the spread operator for immutable updates.