Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

setState unexpectedly updates non-state properties

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.

like image 528
Phi Hong Avatar asked Oct 17 '22 14:10

Phi Hong


1 Answers

Because you aren't intercepting the change in state, it is causing a re-render, which in turn is causing your incremented instance property to be used. This is by design. Any changes to React state will cause the component to re-render, unless you are using a lifecycle hook to control whether or not that should happen.

See https://reactjs.org/docs/react-component.html#shouldcomponentupdate

like image 194
Steve Vaughan Avatar answered Oct 20 '22 18:10

Steve Vaughan