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!
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} />
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.
Related
I'm a newbie in React. I have 6 divs and whenever I call foo() I want to add a number to the first div that's empty.
For example, let's say that the values of the six divs are 1,2,0,0,0,0 and when I call foo(), I want to have 1,2,3,0,0,0.
Here is what I've tried:
var index = 1;
function foo() {
let var x = document.getElementsByClassName("square") // square is the class of my div
x[index-1].innerHTML = index.toString()
index++;
}
I don't know when I should call foo(), and I don't know how should I write foo().
The "React way" is to think about this is:
What should the UI look like for the given data?
How to update the data?
Converting your problem description to this kind of thinking, we would start with an array with six values. For each of these values we are going to render a div:
const data = [0,0,0,0,0,0];
function MyComponent() {
return (
<div>
{data.map((value, i) => <div key={i}>{value}</div>)}
</div>
);
}
ReactDOM.render(<MyComponent />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Now that we can render the data, how are we going to change it? From your description it sounds like every time a function is called, you want change the first 0 value in the array to another value. This can easily be done with:
// Find the index of the first 0 value
const index = data.indexOf(0);
if (index > -1) {
// if it exists, update the value
data[index] = index + 1;
}
To make this work properly with React we have to do two things: Keep track of the updated data in state, so that React rerenders the component when it changes, and update the data in a way that creates a new array instead of mutating the existing array.
You are not explaining how/when the function is called, so I'm going to add a button that would trigger such a function. If the function is triggered differently then the component needs to be adjusted accordingly of course.
function update(data) {
const index = data.indexOf(0);
if (index > -1) {
data = Array.from(data); // create a copy of the array
data[index] = index + 1;
return data;
}
return data;
}
function MyComponent() {
var [data, setData] = React.useState([0,0,0,0,0,0]);
return (
<div>
{data.map((value, i) => <div key={i}>{value}</div>)}
<button onClick={() => setData(update(data))}>Update</button>
</div>
);
}
ReactDOM.render(<MyComponent />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
You would use state to hold the value and then display the value of that variable.
If you're using functional components:
const App = () => {
const [values, setValues] = React.useState([0, 0, 0, 0, 0, 0]);
const [index, setIndex] = React.useState(0);
const foo = () => {
const tempValues = [...values];
tempValues[index] = index;
setValues(tempValues);
setIndex((index + 1) % values.length);
}
return (
<div>
{ values.map((value) => <div key={`square-${value}`}>{value}</div>) }
<button onClick={ foo }>Click me</button>
</div>
);
};
In class-based components:
constructor(props) {
super(props);
this.state = {
values: [0, 0, 0, 0, 0, 0],
index: 0
};
this.foo = this.foo.bind(this);
}
foo() {
const tempValues = [...values];
const newIndex = index + 1;
tempValues[newIndex] = newIndex;
this.setState({
values: tempValues,
index: newIndex
});
}
render() {
return (
<div>
{ values.map((value) => <div key={`square-${value}`>value</div>) }
<button onClick={ this.foo}>Click me</button>
</div>
);
}
If you need to set the innerHTML of a React component, you can try this:
return <div dangerouslySetInnerHTML={foo()} />;
the foo() here returns the value you want to post in the div.
But in my opinion, your way of thinking on this problem is wrong.
React is cool, but the logic is a bit different of common programming :D
The ideal approach would be to have the divs created by React (using its render method). Then you can pass a variable from array, which is stored in your state. You then just need to change this array within the state and it'll reflect in your view. If you need a working example, just let me know.
However, if you want to update the divs that are not created using react, then you need to use a dirty approach. I would suggest not to use react if you can't generate the view from react.
React is good to separate the concerns between the view and the data.
So the concept of state for this example is useful to store the data.
And the JSX, the React "template" language, to display the view.
I propose this solution:
import React from "react";
class Boxes extends React.Component {
state = {
divs: [1, 2, 3, 0, 0, 0]
};
add() {
// get the index of the first element equals to the condition
const index = this.state.divs.findIndex(elt => elt === 0);
// clone the array (best practice)
const newArray = [...this.state.divs];
// splice, i.e. remove the element at index AND add the new character
newArray.splice(index, 1, "X");
// update the state
// this is responsible, under the hood, to call the render method
this.setState({ divs: newArray });
}
render() {
return (
<div>
<h1>Boxes</h1>
{/* iterate over the state.divs array */}
{this.state.divs.map(function(elt, index) {
return (
<div
key={index}
style={{ border: "1px solid gray", marginBottom: 10 }}
>
{elt}
</div>
);
})}
<button onClick={() => this.add()}>Add a value</button>
</div>
);
}
}
export default Boxes;
I have an object State().score that should be updated with it's added value when it is being called in Handler().addToScore(). The thing is that it always stays at it's initial value which is 0.
const DOM = () => {
const dom = {}
dom.score = document.getElementsByClassName('score')[0]
return dom
}
const State = () => {
const state = {}
state.score = 0 // This remains 0, I want it to update when adding to it
return state
}
const Handler = () => {
const handler = {}
handler.addToScore = function() {
State().score += 10
console.log(State().score) // Equals to 0 on every click and never gets updated
DOM().score.innerHTML = State().score
}
return handler
}
function checkLoginState() {
FB.getLoginStatus(function(response) {
statusChangeCallback(response);
});
}
function statusChangeCallback(response) {
if(response.status === 'connected') {
console.log( 'Logged in and authenticated' )
Handler().addToScore()
} else {
console.log('Not authenticated')
}
}
Every-time you run State() it sets returns a fresh object with score being 0
You will have to save the outcome of the intialization of your State().score if you want it to be saved. Or, you can change up the way that you're generating score by using a getter and a setter
const State = () => {
const state = {}
state.score = 0 // This remains 0, I want it to update when adding to it
return state
}
console.log(State().score); // State() is a function that ALWAYS returns { score: 0 }
let savedScore = State().score;
savedScore += 1;
console.log(savedScore);
Example using get/set (there's multiple ways to do this:
https://jsfiddle.net/mswilson4040/1ds8mbqw/3/
class State {
constructor() {
this._score = 0;
}
get score() {
return this._score;
}
set score(val) {
this._score = val;
}
}
const state = new State();
console.log(state.score);
state.score += 1;
console.log(state.score);
One other way to do this, of course, it to not make State a function. It looks like you're actually trying to manage a score or state so having State be a function that ultimately gives you a brand new state (score) everytime isn't going to work.
Something as simple as just not having State be a function would also work:
const State = () => {
const state = {}
state.score = 0 // This remains 0, I want it to update when adding to it
return state
}
Should be
const State = {
score: 0
};
This question already has answers here:
setState doesn't update the state immediately [duplicate]
(15 answers)
Closed 4 years ago.
Im a noob in React and trying to make a simple app for water phases where the user enters a number and then based on the value it should display the state of water, for example if he enters 212 it should say gas and for 12 it should say solid, but for some reason its not displaying the values correctly, Any help is greatly appreciated!!!
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
msg: "liquid",
temp: 0
};
this.handlenum1Change = this.handlenum1Change.bind(this);
}
handlenum1Change(evt) {
console.log(evt.target.value);
this.setState({
temp: Number(evt.target.value)
});
let temp = this.state.temp
if (temp > 100) {
this.setState({
msg: "boiling"
})
} else if (temp < 32) {
this.setState({
msg: "frozen"
})
} else {
this.setState({
msg: "liquid"
})
}
}
render() {
return (
<div>
<h1> {this.state.msg} </h1>
<form className="form-inline">
<div className="form-group">
<label> Temp: </label>
<input type="number" onChange={this.handlenum1Change} className="form-control" />
</div>
</form>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
setState is asynchronous and won't update the state straight away. It collects multiple state changes before updating.
That means, that this.state won't hold your new value right away.
Or to quote the React docs here:
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
Instead, do it the other way around and work with the user input before setting the new state. That way you can also collectively set both, the temperature and the message at once:
const temp = Number(evt.target.value);
let msg = '';
if (temp > 100) {
msg = 'boiling';
} else if (temp < 32) {
msg = 'frozen';
} else {
msg = 'liquid';
}
this.setState({
temp,
msg,
});
setState is asynchronous. Separately, if you set state (setState({msg: ...})) based on current state (this.state.temp), you must use the callback version of setState.
But in this case you can just set temp and msg at the same time, since they're both working from something outside of state (the temp from the input):
handlenum1Change(evt) {
console.log(evt.target.value);
const temp = Number(evt.target.value);
let msg;
if (temp > 100) {
msg = "boiling";
} else if (temp < 32) {
msg = "frozen";
} else {
msg = "liquid";
}
this.setState({temp, msg});
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
msg: "liquid",
temp: 0
};
this.handlenum1Change = this.handlenum1Change.bind(this);
}
handlenum1Change(evt) {
console.log(evt.target.value);
const temp = Number(evt.target.value);
let msg;
if (temp > 100) {
msg = "boiling";
} else if (temp < 32) {
msg = "frozen";
} else {
msg = "liquid";
}
this.setState({temp, msg});
}
render() {
return (
<div>
<h1> {this.state.msg} </h1>
<form className="form-inline">
<div className="form-group">
<label> Temp: </label>
<input type="number" onChange={this.handlenum1Change} className="form-control" />
</div>
</form>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
setState is not an imperative method to change the state immediately but a request you make to React to change the state of your component as soon as it can (see here).
Another important thing you should factor in is that the event React uses is not a standard HTML Event but rather a SyntheticEvent created by React itself to wrap the standard event, and this SyntheticEvent will be discarded as soon as possible for "performance reasons".
Now, your problem is easily solved by changing
const temp = this.state.temp
To
const temp = Number(evt.target.value)
but you should keep in mind the two above factors when using a React Component's state.
Edit: The consideration about SyntethicEvents is especially important when using the callback version of setState, since accessing the value from within that callback will most likely not contain the value of the input that fired the onChange event, so you have to store it in a variable in the scope outside the callback, for example:
handleInputChange = (event) => {
const value = event.target.value
this.setState(prevState => {
return { myValue: prevState.value + value }
})
}
Since you are not using this.state.temp anywhere else except comparing, you can assign the value in a variable and compare it. FYI, setState wont reflect the changes immediately.
handlenum1Change(evt) {
let temp = evt.target.value;
if (temp > 100) {
this.setState({
msg: "boiling"
})
} else if (temp < 32) {
this.setState({
msg: "frozen"
})
} else {
this.setState({
msg: "liquid"
})
}
}
I am using mobx + react setup for this subpage to make a searchable list of users. My list of items is not rendering with if statement. In the solution I am trying to render one of two lists in my subpage. Depending on boolean 'isSearching'. First element should be shown when input field is empty, and second one is when input field has value written. They are same arrays, Only difference between lists arrays are that one of them is filtered.
Code:
<ul className='items__block'>
{
this.props.PeopleStore.people.isSearching = false ?
(this.props.PeopleStore.people.map(this.person))
:
(this.props.PeopleStore.searchingList.map(this.person))
}
</ul>
Althought if I remove condition, it works separated:
<ul className='items__block'>
{
this.props.PeopleStore.people.map(this.person)
}
</ul>
<ul className='items__block'>
{
this.props.PeopleStore.people.map(this.person)
}
</ul>
Store file:
import { runInAction, observable, action, toJS } from 'mobx';
// ES7 compiler
import regeneratorRuntime from 'regenerator-runtime';
class PeopleStore {
#observable people = [];
#observable loading = false;
#observable isSearching = false;
#observable searchingList = [];
// API call
loadPeople = async() => {
this.loading = true;
const response = await fetch('https://randomuser.me/api/?results=71');
const json = await response.json();
runInAction(() => {
this.people = json.results;
});
this.loading = false;
console.log(toJS(this.people));
}
// this function is called by onChange event
#action.bound filterList = textTyped => {
// changing boolean to identify if input is empty or not
if (textTyped.target.value.length < 1) {
this.isSearching = false;
} else {
this.isSearching = true;
}
console.log(this.isSearching);
let peoplesNames = [];
for (let i = 0; i < this.people.length; i++) {
peoplesNames.push(toJS(this.people[i]));
}
peoplesNames = peoplesNames.filter(function(item) {
return item.name.first.toLowerCase().search(textTyped.target.value.toLowerCase()) !== -1
});
this.searchingList = peoplesNames;
// tracking both arrays, they both work
console.log(toJS(this.searchingList));
console.log(toJS(this.people));
}
}
export default new PeopleStore();
Component file:
#inject('showHandler', 'PeopleStore') #observer
class PickList extends React.Component {
componentWillMount() {
this.props.PeopleStore.loadPeople();
}
person = ({name, picture}, index) =>
<li className="items__block--user" key={index} onClick={this.props.PeopleStore.selectPerson}>
<img className="user--image" src={picture.medium} alt="face" />
<span className="user--name">{`${name.first} ${name.last}`}</span>
</li>;
render() {
if (this.props.PeopleStore.loading) {
return (
<div className="loader"></div>
);
}
return (
<React.Fragment>
<input className="users__block--input" onChange={this.props.PeopleStore.filterList}></input>
<ul className='items__block'>
{
this.props.PeopleStore.people.isSearching = false //checks mobx prop
?
(this.props.PeopleStore.people.map(this.person))
:
(this.props.PeopleStore.searchingList.map(this.person))
}
</ul>
Why is it not working? On page render isSearching prop is set to false and that should effect the if statement as it is.
Issue is here, you are not checking the condition properly:
this.props.PeopleStore.people.isSearching = false
It should be:
this.props.PeopleStore.people.isSearching == false // notice "=="
See what will happen with =, it will assign the value returned by ternary operator expression to isSearching variable. It will be treated like this:
isSearching = (false? 1: 2); // isSearching will get a new value, and second expression will get executed always
Check this snippet:
let b = false;
b = false? 1: 2; //b will become 2
console.log('b = ', b);
How to write this without using JSX?
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList />
<CommentForm />
</div>
);
}
});
This comes from the react.js tutorial: http://facebook.github.io/react/docs/tutorial.html
I know I can do the following:
return (
React.createElement('div', { className: "commentBox" },
React.createElement('h1', {}, "Comments")
)
But this only adds one element. How can I add more next to one another.
You can use the online Babel REPL (https://babeljs.io/repl/) as a quick way to convert little chunks of JSX to the equivalent JavaScript.
var CommentBox = React.createClass({displayName: 'CommentBox',
render: function() {
return (
React.createElement("div", {className: "commentBox"},
React.createElement("h1", null, "Comments"),
React.createElement(CommentList, null),
React.createElement(CommentForm, null)
)
);
}
});
It's also handy for checking what the transpiler outputs for the ES6 transforms it supports.
insin's answer is the direct translation, however you may prefer to use factories.
var div = React.createFactory('div'), h1 = React.createFactory('h1');
var CommentBox = React.createClass({displayName: 'CommentBox',
render: function() {
return (
div({className: "commentBox"},
h1(null, "Comments"),
React.createElement(CommentList, null),
React.createElement(CommentForm, null)
)
);
}
});
createFactory essentially partially applies createElement. So the following are equivalent:
React.createElement(c, props, child1, child2);
React.createFactory(c)(props, child1, child2);
If you're just using es6 but aren't fond of JSX you can make it less verbose with destructuring assignment. See this jsbin for an interactive example using 6to5 instead of jsx.
var [div, h1, commentForm, commentList] = [
'div', 'h1', CommentForm, CommentList
].map(React.createFactory);
if you have a variable number of children then you can use that:
Using apply function which take an array of parameters.
React.createElement.apply(this, ['tbody', {your setting}].concat(this.renderLineList()))
where renderLineList is for instance:
renderLineList: function() {
var data=this.props.data;
var lineList=[];
data.map(function(line) {
lineList.push(React.createElement('tr', {your setting}));
});
return lineList;
}
You just add them one after another as children to your parent component,
return React.createElement("div", null,
React.createElement(CommentList, null),
React.createElement(CommentForm, null)
);
I had this problem, it took a while to solve by stepping through the interpreter source code:
var arrayOfData = [];
var buildArray = (function () {
var id;
var name;
return{
buildProc(index, oneName){
id = index;
name = oneName;
arrayOfData[index] = (React.createElement('Option', {id},name));
}
}
})();
// then
this.state.items = result;
var response = parseJson.parseStart(this.state.items);
var serverDims = response.split(":");
for (var i = 1; i < serverDims.length; i++) {
buildArray.buildProc(i, serverDims[i] )
}
// then
render(){
const data = this.props.arrayOfData;
return (
React.createElement("select", {},
data
)
// {data} Failed with "object not a valid React child, data with no curly's worked
)
}