React: setState unexpectedly update non-state properties

Created on 9 Aug 2018  路  2Comments  路  Source: facebook/react

I don't know if this is a known issue or an intended feature, but I have found an interesting problem.

So we all know that if we want to render a reactive value in React, we have to put the value in the state and use setState:

constructor() {
  super();
  this.state = { counter: 0 }
  this.incrementButtonListener = (e) => {
    e.preventDefault();
    this.setState(prevState => ({ counter: prevState.counter + 1 }));
  };
}

render() {
  return (
    <div>
      <h1>{this.state.counter}</h1>
      // When clicked, counter increments by 1 and re-renders
      <button onChange={this.incrementButtonListener}>Increment</button> 
    </div>
  )
}

But if we make counter as a field property, render() will only catch a snapshot of counter when the component is created, and even when counter is incremented, the result will not be displayed reactively in render():

constructor() {
  super();
  this.counter = 0;
  this.incrementButtonListener = (e) => {
    e.preventDefault();
    this.counter++;
  };
}

render() {
  return (
    <div>
      <h1>{this.counter}</h1>
      // When clicked, counter increments by 1 but the difference is NOT rendered
      <button onChange={this.incrementButtonListener}>Increment</button> 
    </div>
  )
}

Right? Basic stuff.

However, there's an interesting occurence when I try to fiddle around with this code. We keeps counter as a field property and everything else intact. The only difference is that, in the incrementButtonListener, I'm going to add a setState on someStateProperty:

constructor() {
  super();
  this.counter = 0;
  this.incrementButtonListener = (e) => {
    e.preventDefault();
    this.counter++;
    /*-------------------------------ADD THIS*/
    this.setState({});
    // You have to pass an object, even if it's empty. this.setState() won't work.
    /*-----------------------------------------*/
  };
}

render() {
  return (
    <div>
      <h1>{this.counter}</h1>
      // Surprise surprise, now this.counter will update as if it was in the state! 
      <button onChange={this.incrementButtonListener}>Increment</button> 
    </div>
  )
}

This time, this.counter updates as if it was in the state!

So my assumption is, every time setState is called (and even with an empty object as a parameter), render() runs again and this.counter will get recalculated and, thus, incremented. Of course, it won't be 100% as reactive as a state property. But, in this use case, the only time this.counter would change is when I click on the Increment button. So, if I put a setState in the listener, it would work as if this.counter is in the state.

Now, I'm not sure if this is an accepted behavior or just an unexpected hack, and whether I should make use of it or not. Could anybody help me elaborate this?

Here is a fiddle if you want to see the behavior in action. You can comment out the this.setState({}) bit in line 7 to see the difference.

Most helpful comment

What @Yurickh said is right.

I strongly recommend to avoid code like this because it's easy to break. If something changes, keep it in the state, and call setState to explicitly ask for an update.

Cheers!

All 2 comments

Hi phiboi!

What is happening here is that your render method is called every time your component updates, and it happens to update whenever props or state are changed.

Just like you said, render will catch a "snapshot" of the property when it runs, and it won't trigger a re-render when you change it.

BUT, when you call this.setState({}), you're effectively trying to update state, which triggers a re-render, then your render method will be called again, taking another snapshot of the value in this.counter, updating your view.
This is why it seems like this.counter is in your state.

Using properties like that is quite brittle, tho, so I wouldn't recommend to go down this line.

Hope I got the message across. Cheers!

What @Yurickh said is right.

I strongly recommend to avoid code like this because it's easy to break. If something changes, keep it in the state, and call setState to explicitly ask for an update.

Cheers!

Was this page helpful?
0 / 5 - 0 ratings