1

It's said that React components' states should be treated as if they are immutable, and that mutating this.state can be error-prone even if you immediately call setState. That is,

// #1: bad...
this.state.items.push("New item");
this.setState({items: this.state.items});

// #2: good...
this.setState({items: this.state.items.concat(["New item"])});

Can someone elaborate on what exactly would cause the first version above to fail?

Community
  • 1
  • 1
Andrew Magee
  • 6,506
  • 4
  • 35
  • 58

1 Answers1

4

When you call setState with a new value, some components within React can compare the new state to the previous state to detect changes for performance enhancement (see here: https://facebook.github.io/react/docs/advanced-performance.html). The method is called shouldComponentUpdate, and receives the next props and state for comparison. Some frameworks like Omniscient and Om use this.

In your first example you've mutated the state already (objects and arrays are by-reference in JS), and so calling setState may be a no-op (as the state is already the same). Your component might not automatically re-render.

Here's an example:

// Let's assume that `this.state === {numbers: [1]};` at the start
this.state.numbers.push(2);

// Oops - we just changed this.state directly,
// so now this.state === `{numbers: [1, 2]}`

// Now, react might compare the old this.state (which has been modified to [1, 2]) 
// to the object you're passing in (which is also [1, 2]).
// This might be a no-op.
this.setState({numbers: this.state.numbers});

In the second example MDN says that "The concat() method returns a new array comprised...", that is — you're not modifying the original, you're returning a new array entirely. In the first example, .push edits the existing array.

Because it's a new array the comparison will always run as expected.

tonyhb
  • 3,706
  • 3
  • 20
  • 16
  • 1
    I'm not sure that's the issue. From what I can see, `render` is always called when `setState` is called, even when all the attributes passed in are identical to their current values (ie. I can repeatedly call `this.setState({a: 2})` and it will call `render` each time). – Andrew Magee Aug 04 '15 at 03:59
  • To expand a little more, this is the issue and the answer is hidden here: https://facebook.github.io/react/docs/advanced-performance.html - if you mutate directly and then call setState() things which rely on comparing state break. You are right in that render() is always called. I've updated the answer to reflect. – tonyhb Aug 05 '15 at 02:08
  • Ok, I can understand that my "good" and "bad" versions would behave differently in the presence of certain `shouldComponentUpdate` implementations. Is that the only issue? – Andrew Magee Aug 05 '15 at 02:34
  • As far as I know that's the only concrete issue, though it is an antipattern because modifying state isn't done in functional programming. In React it's a good aim to make functions pure. An example of pure stores in Flux is the [Redux](https://github.com/gaearon/redux) framework — it's excellent. – tonyhb Aug 05 '15 at 03:06
  • Ok, I think I'm finally satisfied with this answer :) – Andrew Magee Aug 05 '15 at 07:09