backdesk
backdesk

Reputation: 1781

React.js Understanding setState

I've been having a go at learning React.js by writing a small calculator application. I thought things were going quite well until I learned that setState is asynchronous and my mutations therefore do not get immediately applied.

So my question is, what is the best way to keep a running total based upon the values being added to an input. Take the following example:

var Calculator = React.createClass({
  total : 0,

  getInitialState : function(){
    return { 
      value : '0'
    };
  },

  onValueClicked : function (value) {
    var actual, total, current = this.state.value;

    if(value === '+') {
      actual = this.total = parseInt(this.total, 10) + parseInt(current, 10);
    } else {
      if(parseInt(current, 10) === 0) {
        actual = value;
      } else {
        actual = current.toString() + value;
      }
    }

    this.setState({ value : actual });
  },

  render : function () {
    return (
      <div className="calc-main">
        <CalcDisplay value={this.state.value} />
        <CalcButtonGroup range="0-10" onClick={this.onValueClicked} />
        <CalcOpButton type="+" onClick={this.onValueClicked} />
      </div>
    )
  }
});

var CalcDisplay = React.createClass({
  render : function () {
    return (
      <input type="text" name="display" value={this.props.value} />
    );
  }
});

var CalcButtonGroup = React.createClass({
  render : function () {
    var i, buttons = [], range = this.props.range.split('-');

    for(i = range[0]; i < range[1]; i++) {
      var handler = this.props.onClick.bind(null, i);

      buttons.push(<CalcNumberButton key={i} onClick={ handler } />);
    }

    return (
      <div className="calc-btn-group">{ buttons }</div>
    );
  }
});

var CalcNumberButton = React.createClass({
  render : function () {
    return (
      <button onClick={this.props.onClick}>{this.props.key}</button>
    );
  }
});

var CalcOpButton = React.createClass({
  render : function () {
    var handler, op = this.props.type;

    handler = this.props.onClick.bind(null, op);

    return (
      <button onClick={handler}>{op}</button>
    );
  }
});

React.renderComponent(<Calculator />, document.getElementById('container'));

In the example above I gave up completely on storing the total within the state and kept it outside. I've read that you can have a callback run when setState has finished but in the case of a calculator I need it to be snappy and update quickly. If the state isn't getting updated with each button press and I quickly hit the buttons - things are going to fall out of sync. Is the callback all I am missing or am I thinking about this in completely the wrong way?

Any help appreciated!

Upvotes: 1

Views: 2186

Answers (2)

Junle Li
Junle Li

Reputation: 1035

You have define the total variable for your business logic state. Why not store more information like that?

var Calculator = React.createClass({
  previous: 0, // <-- previous result
  current: 0, // <-- current display
  op: '', // <-- pending operator

  getInitialState : function(){
    return { 
      value : '0'
    };
  },

  onValueClicked : function (value) {
    var actual;

    if(value === '+') {
      this.previous = this.current;
      this.op = '+';
      actual = 0; // start a new number
    } else if (value === '=') {
      if (this.op === '+') {
        actual = this.previous + this.current;
      } else {
        actual = this.current; // no-op
      }
    } else {
      actual = current * 10 + value;
    }

    this.current = actual; // <-- business logic state update is synchronous
    this.setState({ value : String(actual) }); // <-- this.state is only for UI state, asynchronous just fine
  },

  render : function () {
    return (
      <div className="calc-main">
        <CalcDisplay value={this.state.value} />
        <CalcButtonGroup range="0-10" onClick={this.onValueClicked} />
        <CalcOpButton type="+" onClick={this.onValueClicked} />
        <CalcOpButton type="=" onClick={this.onValueClicked} />
      </div>
    )
  }
});

The basic idea to resolve this.state is use other variables to store your business logic state, and reserve this.state only for UI state.

PS. A real calculator has more complex business logic than this. You should define every state and state machine clearly in spec.

Upvotes: 0

Brigand
Brigand

Reputation: 86270

It's asynchronous, but much faster than the fastest possible human click.


Aside from that, you should declare instance variables in componentDidMount, e.g.

componentDidMount: function(){
  this.total = 0;
}

... but in this case you probably want to store it in state.


.split returns an array of strings, you want to be using numbers:

range = this.props.range.split('-').map(Number)

Or avoid the strings altogether (prefered) with one of these:

<CalcButtonGroup range={[0, 10]} onClick={this.onValueClicked} />
<CalcButtonGroup range={{from: 0, till: 10}} onClick={this.onValueClicked} />

Upvotes: 2

Related Questions