6

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.

Phi Hong
  • 231
  • 3
  • 8
  • 1
    https://stackoverflow.com/questions/24718709/reactjs-does-render-get-called-any-time-setstate-is-called – stackoverfloweth Aug 09 '18 at 15:31
  • Frankly I don't see _unexpected_ behavior here. React just ensures (by default) that `render` is called after each `setState` call. If you change a member property alongside `setState` call, there's _no surprise_ that `render` outputs _updated_ property's value (since it changes before `setState`). – hindmost Aug 09 '18 at 15:50
  • 1
    you can force render with `this.forceUpdate();` - no need to call it by setState . It's (well?) known possibility but considered as antipattern. – xadm Aug 09 '18 at 16:01

4 Answers4

1

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

Steve Vaughan
  • 2,163
  • 13
  • 18
1

React use this.state and setState because it has some own purpose.

By using React state mechanism, we got benefit like the verbosity, functional-like programming style which function should be pure and not mutate the data.

Every time you call setState it will use the new value instead mutate the existing and make the behavior unpredictable.

Also you could benefit to prevent component re-render or update by utilize shouldComponentUpdate which check props/state inside it.

After your apps is getting complicated, then library like Redux could save your ass in the future. For simpler component or apps, React state is enough.

Further reading:

https://reactjs.org/docs/faq-state.html

https://reactjs.org/docs/state-and-lifecycle.html

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

https://spin.atomicobject.com/2017/06/07/react-state-vs-redux-state/

I Putu Yoga Permana
  • 3,980
  • 29
  • 33
1

You can replace setState on forcrUpdate. If your team use decorative style of code, then never use setState({}) or forceUpdate(). React's authors recommendes make components like pure functions.

Gothic Prince
  • 113
  • 10
0
class Hello extends React.Component {
    state = { counter: 0 };

    increment = () => {
        const { counter } = this.state;

        this.setState({ counter: counter + 1 });
    };

    decrement = () => {
        const { counter } = this.state;

        this.setState({ counter: counter - 1 });
    };

    render() {
        const { counter } = this.state;

        return (
            <div>
                <h1>{counter}</h1>

                <p>
                    <button onClick={this.increment}>Increment</button>
                </p>
                <p>
                    <button onClick={this.decrement}>Decrement</button>
                </p>
            </div>
        );
    }
}

ReactDOM.render(<Hello />, document.getElementById("container"));

I hope this helps you pick react up! let me know if you have any questions :)

Neil
  • 971
  • 2
  • 12
  • 33