Reputation: 1781
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
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
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