Reputation: 1083
EDIT: since the code snip does not reproduce the bug - here is a link to the github repo: (code is far FAR from complete)
https://github.com/altruios/clicker-game
I have run it on two computers now - both with the same behavior the code snip doesn't show.
//interestingly enough, this works just fine, where the same code I run locally has the doubling.
//when I comment out ALL other code except for this code I STILL get the error locally
//at this point the only difference is import export of components... here they are in one file.
//below is original code from file (
/*
FILE::::Clicker.js
import React from 'react';
function Clicker(props)
{
return(
<div>
{props.name}
<button
name={props.name}
onClick={props.HandleClick}
data-target={props.subjectsOfIncrease}>
{props.name} {props.value}
</button>
</div>
)
}
export default Clicker;
FILE:: Resouce.js
import React from 'react';
function Resource(props)
{
return(
<div>
{props.name} and {props.amount || 0}
</div>
)
}
export default Resource;
*/
//besides the import/export and seprate files - code is the same. it works in here, does not work locally on my machine.
const gameData = {
clickerData: [{
name: "grey",
subjectsOfIncrease: ["grey"],
isUnlocked: true,
value: 1
}],
resourceData: [{
name: "grey",
resouceMax: 100,
isUnlocked: true,
changePerTick: 0,
counterTillStopped: 100,
amount: 0
}]
}
class App extends React.Component {
constructor() {
super();
this.state = {
resources: gameData.resourceData,
clickers: gameData.clickerData
};
this.gainResource = this.gainResource.bind(this);
}
gainResource(event) {
console.count("gain button");
const name = event.target.name;
this.setState((prevState) => {
const newResources = prevState.resources.map(resource => {
if (resource.name === name) {
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
console.log(prevState.resources.find(item => item.name === name).amount, "old");
console.log(newResources.find(item => item.name === name).amount, "new");
return {
resources: newResources
}
});
}
render() {
const resources = this.state.resources.map(resourceData => {
return (
<Resource
name = {resourceData.name}
resouceMax = {resourceData.resourceMax}
isUnlocked = {resourceData.isUnlocked}
changePerTick = {resourceData.changePerTick}
counterTillStopped = {resourceData.countTillStopped}
amount = {resourceData.amount}
key = {resourceData.name}
/>
)
})
const clickers = this.state.clickers.map(clickerData => {
return (
<Clicker
name = {clickerData.name}
HandleClick = {this.gainResource}
value = {clickerData.amount}
key = {clickerData.name}
/>
)
})
return (
<div className = "App" >
{resources}
{clickers}
</div>
)
}
}
function Resource(props) {
return <div > {props.name} and {props.amount || 0} </div>
}
function Clicker(props) {
return (
<div > {props.name}
<button name = {props.name} onClick = {props.HandleClick}>
{props.name} {props.value}
</button>
</div>
)
}
const root = document.getElementById('root');
ReactDOM.render( <App / >,root );
<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>
<div id="root"></div>
so I'm building a clicker game to learn react, and something I don't understand why this code is behaving the way it does:
in the main app I have this function:
gainResource(event)
{
console.count("gain button");
const name = event.target.name;
this.setState( (prevState)=>
{
const newResources = prevState.resources.map(resource=>
{
if(resource.name === name)
{
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
console.log(prevState.resources.find(item=>item.name===name).amount, "old");
console.log(newResources.find(item=>item.name===name).amount, "new");
return {resources: newResources}
});
}
that console.count runs a single time... but I get 2 'old and new' pairs. as if setState is running twice in this function which only runs once?
the console.output is:
App.js:64 gain button: 1
App.js:76 1 "old"
App.js:77 1 "new"
App.js:76 2 "old"
App.js:77 2 "new"
so it looks like the function is running once. but the set state is being run twice?
the symptoms are that it counts up by 2. but also the initial state of amount is 0, not 1, as seen in the gamedata.json
resourceData:
[
{
name:"grey",
resouceMax:100,
isUnlocked:true,
changePerTick:0,
counterTillStopped:100,
amount:0
},{etc},{},{}],
clickerData:
[
{
name:"grey",
subjectsOfIncrease:["grey"],
isUnlocked:true,
value:1
},{etc},{},{}]
i don't think the rest of the code I'm about to most is relevant to this behavior, but I don't know react yet, so I don't know what I'm missing: but this is how I'm generating the clicker button:
const clickers = this.state.clickers.map(clickerData=>
{
return(
<Clicker
name={clickerData.name}
HandleClick = {this.gainResource}
value = {clickerData.amount}
key={clickerData.name}
/>
)
})
and in the clicker.js functional component I'm just returning this:
<div>
{props.name}
<button name={props.name} onClick={props.HandleClick}>
{props.name} {props.value}
</button>
</div>
the function is bound to this in the constructor... I don't understand why this is running setState twice inside a function that's called once.
I've also tried:
<div>
{props.name}
<button name={props.name} onClick={()=>props.HandleClick}> //anon function results in no output
{props.name} {props.value}
</button>
</div>
Upvotes: 11
Views: 18387
Reputation: 447
Ok took me a bit of time to figure this out. As others have mentioned your call back needs to be idempotent. the thing to realise here is that react passes the same state object instance into your callback each time it calls it. hence if you change the state object on the first call it will be different on the second call and your callback function will not be idempotent.
this.setState((state) =>
{
//state.counter will have the same value on the first and second
//time your callback is called
return { counter: state.counter + 1};
});
this.setState((state) =>
{
//state.counter will have a value of n+1 the second time it is called
//because you are changing the sate object. This will have the net effect
//of incrementing state.counter by 2 each time you call this.setState!!
// In this case, state is being mutated directly, which you want to
// avoid in setState callback functions along with other side effects.
// Callback functions passed into setState should be pure functions.
// an object returned by a callback function
state.counter = state.counter + 1;
return { counter: state.counter};
});
The above is probably obvious but this situation becomes less obvious when dealing with arrays. for eg
this.setState((state) =>
{
//even though we are creating a new array, the
//object references, not the object values themselves,
//in the array have just been copied
//so changing them is problematic
newArray = [...state.someArray];
//this is ok as we are replacing the object at newArray[1]
newArray[1] = {objectField : 1};
//this is not ok
newArray[1].objectField = 1;
return { someArray: newArray};
});
Upvotes: 6
Reputation: 1270
This is an intended behavior of a setState(callback) method wrapped in a <React.Strict> component.
The callback is executed twice to make sure it doesn't mutate state directly.
as per: https://github.com/facebook/react/issues/12856#issuecomment-390206425
In the snippet, you create a new array, but the objects inside of it are still the same:
const newResources = lastResources.map(resource => {
if(resource.name === name){
resource.amount = Number(resource.amount) + 1
}
return resource;
}
You have to duplicate each object individually:
const newResources = lastResources.map(resource => {
const newObject = Object.assign({}, resource)
if(resource.name === name){
newObject.amount = Number(newObject.amount) + 1
}
return newObject;
}
Upvotes: 23
Reputation: 1083
BEST ANSWER:
I was using create-react-app. and my App Component was wrapped in Strict mode... which fires setState twice... which perfectly explains why this was not reproducible on the code snip, and why the function was being called once, yet setState was called twice.
removing strict mode fixed the issue completely.
Upvotes: 9
Reputation: 1083
okay... so after some hair pulling... I found out a way that works... but I DON'T think this is 'best practice' but it now works for me when I write this:
gainResource(event)
{
const name = event.target.name;
const lastResources = this.state.resources.slice();
const newResources = lastResources.map(resource=>
{
if(resource.name === name)
{
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
this.setState({resources: newResources});
}
vs
gainResource(event)
{
console.count("gain button");
const name = event.target.name;
this.setState( (prevState)=>
{
const newResources = prevState.resources.map(resource=>
{
if(resource.name === name)
{
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
console.log(prevState.resources.find(item=>item.name===name).amount, "old");
console.log(newResources.find(item=>item.name===name).amount, "new");
return {resources: newResources}
});
}
that setState without the function of prevState is called once... whereas with the prevState it's called twice... why?
so I still don't understand why setState using a function with prevState is causing two function calls within a function that's called only once... I have read that I should be using prev state as this.state.resources.slice(); just takes an 'untimed snapshot' and could be unreliable. is this true... or is this methodology acceptable?
this is AN answer to anyone else struggling with this. hopefully a better answer can be posted after this enlightenment to what might be happening.
Upvotes: 0
Reputation: 136
As long as you didn't provide us a runnable example I've one doubt about what could be happened and let's see if it works.
What I can see is in the gainResource function and specially in this line resource.amount = Number(resource.amount) + 1 you're trying to update the state without using setState which is not recommended by React Documentation
Please instead try first to assign a const myRessource = ressource then return myRessource instead.
gainResource(event)
{
console.count("gain button");
const name = event.target.name;
this.setState( (prevState)=>
{
const newResources = prevState.resources.map(resource=>
{
const myRessource = ressource;
if(myRessource.name === name)
{
myRessource.amount = Number(myRessource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return myRessource;
});
console.log(prevState.resources.find(item=>item.name===name).amount, "old");
console.log(newResources.find(item=>item.name===name).amount, "new");
return {resources: newResources}
});
}
Upvotes: 0