Why Not To Modify React State Directly

By Dave Ceddia Comment

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.

Learning React can be a struggle -- so many libraries and tools!
My advice? Ignore all of them :)
For a step-by-step approach, read my book Pure React.

Loved it! Very well written and put together. Love that you focused only on React. Wish I had stumbled onto your book first before I messed around with all those scary "boilerplate" projects.
— John Lyon-Smith
comments powered by Disqus