React hooks - setState does not update some state properties - javascript

i am making a game but i am having a problem using setState hook on react, it isnt updating my state, my state properties are
const [state,setState] =useState({
score:0,
holes:9,
initGame:false,
lastHole:-1,
minPeepTime: 200,
maxPeepTime: 1000})
i also have 2 useEffect hook just for testing
useEffect(()=>{
console.log("init game");
console.log(state.initGame);
console.log("--------------");
},[state.initGame])
useEffect(()=>{
console.log("last hole");
console.log(state.lastHole);
console.log("##############");
},[state.lastHole])
and i have a function to start the game
const StartGame=()=>{
console.log("here")
setState({...state,initGame: true,score:0,lastHole: 15})
peep();
setTimeout(() => {
console.log("Game Over")
setState({...state,lastHole:-1,initGame: false})
}, 10000)
}
"here" is actually being printed, and when time is over "Game Over" is being printed.
peep() function update the property lastHole and it call useEffect correctly, but property initGame is never being updated by setState and that my actual problem, useEffect for that property is never call (just the first time when my page is loaded, but not on StartGame function) and its value never change
UPD:
this is the peep function.
const randomTime = (min, max) => {
return Math.round(Math.random() * (max - min) + min);
};
const randomHole = () => {
const idx = Math.floor(Math.random() * state.holes);
if (idx === state.lastHole){
return randomHole();
}
setState({...state,lastHole: idx});
}
const peep = () => {
randomHole();
let time = randomTime(state.minPeepTime,state.maxPeepTime)
if(state.initGame){
setTimeout(() => {
peep()
}, time)
}
}
and this is the output of the code

To properly update state in this scenario use:
setState((initialState) => ({
...initialState,
initGame: true,
score: 0,
lastsHole: 15
}))
The setTimeout part becomes
setState((initialState) => ({
...initialState,
lastHole: -1,
initGame: false
}))
I don't know what peek does or the importance of the setTimeout though;

The problem here is with the setState. setState doesn't immediately change the state as you expect.
setState({
...state,
initGame: true,
score: 0,
lastHole: 15,
});
peep() // value of initGame is false
Unexpectedly, the value of initGame will still be false. Change all the setstates like given below.
setState((state) => ({
...state,
initGame: true,
score: 0,
lastHole: 15,
}));
This will successfully set initGame to true before calling peep()
The final output will be as shown below.

Related

Updating a state variable inside of a react function

I'm using the State Hook to update a Throttle setting for a robot based on keyboard commands (W,A,S,D). I have a maxThrottle state variable that ensures that the robot doesn't go too fast. However, I am using a slider to adjust the maxThrottle command. When you press the W key (forward) you should get the current maxThrottle['forward'] value assigned to the throttleCommand variable. However, every time the handleKey function runs, I just get throttleCommand['forward'] set to the initial value of 30, even if I have changed maxThrottle (using setMaxThrottle) to a higher number like 80.
function App(){
//state hooks
const [ throttleCommand, setThrottleCommand ] = useState({ forward: 0, turn: 0 });
const [ maxThrottle, setMaxThrottle ] = useState({ forward: 30, turn: 15 });
useEffect(
() => {
sendThrottle(throttleCommand);
},
[ throttleCommand ]
);
const handleKey = (e) => {
switch (e.code) {
case 'KeyW':
setThrottleCommand({ ...throttleCommand, forward: maxThrottle.forward });
break;
case 'KeyS':
//THIS SHOULD TURN IT OFFF
setThrottleCommand({ forward: 0, turn: 0 });
break;
case 'KeyA':
//turn left
setThrottleCommand({ ...throttleCommand, turn: -maxThrottle.turn });
break;
case 'KeyD':
setThrottleCommand({ ...throttleCommand, turn: maxThrottle.turn });
break;
default:
break;
}
};
const sendThrottle = () => {
//here I test the throttleCommand
console.log('sending command', throttleCommand);
axios.get(`/throttle/${throttleCommand.forward}/${throttleCommand.turn}`).then((res) => {
setThrottleData(res.data);
});
};
....
}
I have verified that I successfully update maxThrottle to {forward:80,turn:20} but when I press the W key, the throttleCommand is logged as {forward:30, turn:0}. I am expecting to see {forward:80,turn:0} assigned to throttleCommand.
Is there something wrong with using a state variable inside the handleKey function? Why am I always getting the initial value of maxThrottle assigned to throttleCommand?
Michael Bauer! Thanks for the fix. You are 100% correct. Code now works. here is the change:
incorrect keydown listener:
useEffect(() => {
document.addEventListener('keydown',handleKey);
}, []);
SOLVED / CORRECT keydown listener
useEffect(() => {
document.addEventListener('keydown', (e) => {
handleKey(e);
});
}, []);
Lesson learned - use arrow functions inside of useEffect so that the callback function updates!

React SetState Overwriting Entire Object Instead of Merging

My state looks like this in the constructor:
this.state = {
selectedFile: null, //current file selected for upload.
appStatus: 'waiting for zip...', //status view
zipUploaded: false,
zipUnpacked: false,
capturingScreens: false,
finishedCapture: false,
htmlFiles: null,
generatedList: [],
optionValues: {
delayValue: 1
},
sessionId: null,
estimatedTime: null,
zippedBackupFile: null,
secondsElapsed:0,
timer: {
screenshotStart:0,
screenshotEnd:0,
timingArray:[],
averageTimePerUnit:25,
totalEstimate:0
}
};
I have the following functions in my app.js:
this.secondsCounter = setInterval(this.countSeconds, 1000); // set inside the constructor
getStateCopy = () => Object.assign({}, this.state);
countSeconds = () => {
let stateCopy = this.getStateCopy();
let currentSeconds = stateCopy.secondsElapsed + 1;
this.setState({secondsElapsed:currentSeconds});
}
captureTime = (startOrStop) => {
let stateCopy = this.getStateCopy();
let secondsCopy = stateCopy.secondsElapsed;
let startPoint;
if(startOrStop === true) {
this.setState({timer:{screenshotStart:secondsCopy}});
} else if(startOrStop === false){
this.setState({timer:{screenshotEnd:secondsCopy}});
startPoint = stateCopy.timer.screenshotStart;
stateCopy.timer.timingArray.push((secondsCopy-startPoint));
this.setState({secondsElapsed:secondsCopy})
stateCopy.timer.averageTimePerUnit = stateCopy.timer.timingArray.reduce((a,b) => a + b, 0) / stateCopy.timer.timingArray.length;
this.setState({secondsElapsed:secondsCopy})
this.setState({timer:{averageTimePerUnit:stateCopy.timer.averageTimePerUnit}})
}
I'm getting an error that "push" does not exist on stateCopy.timer.timingArray. I did some investigation and found that this.setState({timer:{screenshotStart:secondsCopy}}); is actually overwriting the entire "timer" object in state and removing all of the previous properties instead of merging them.
I don't understand what I'm doing wrong.. I'm using stateCopy to avoid mutating state, and to get proper values (avoiding asynchronous confusion). Every article I read online about react suggests that writing an object to state will merge with whatever is already there, so why does it keep overwriting "timer" instead of merging??
I did some investigation and found that this.setState({timer:{screenshotStart:secondsCopy}}); is actually overwriting the entire "timer" object in state and removing all of the previous properties instead of merging them.
Correct. setState only handles merging at the top level. Anything below that you have to handle yourself. For instance:
this.setState(({timer}) => {timer: {...timer, screenshotStart: secondsCopy}});
Note the use of the callback version of setState. It's important to do that any time you're providing state information that's dependent on existing state.
There are other places you have to do the same sort of thing, including when you push to the array. Here are some further notes:
There's no reason to copy state here:
countSeconds = () => {
let stateCopy = this.getStateCopy();
let currentSeconds = stateCopy.secondsElapsed + 1;
this.setState({secondsElapsed: currentSeconds});
}
...and (as I mentioned above) you must use the callback form to reliably modify state based on existing state. Instead:
countSeconds = () => {
this.setState(({secondsElapsed}) => {secondsElapsed: secondsElapsed + 1});
};
Similarly in captureTime:
captureTime = (startOrStop) => {
if (startOrStop) { // *** There's no reason for `=== true`
this.setState(({timer, secondsElapsed}) => {timer: {...timer, screenshotStart: secondsElapsed}});
} else { // *** Unless `startOrStop` may be missing or something, no need for `if` or `=== false`.
this.setState(({timer, secondsElapsed}) => {
const timingArray = [...timer.timingArray, secondsElapsed - timer.screenshotStart];
const update = {
timer: {
...timer,
screenshotEnd: secondsElapsed,
timingArray,
averageTimePerUnit: timingArray.reduce((a,b) => a + b, 0)
}
};
});
}
};
Side note: Your copyState function does a shallow state copy. So if you modify any properties on the objects it contains, you'll be directly modifying state, which you mustn't do in React.
setState hooks overwrite the state with a new object always... that is their correct behavior.
you need to use a function within setState. not just pass in an object.
setState((prevState,prevProps)=>{
//logic to make a new object that you will return ... copy properties from prevState as needed.
//something like const newState = {...prevState} //iffy myself on exact syntax
return newState
})
Your getStateCopy is only shallow cloning the existing state - anything nested is not cloned. To illustrate:
const getStateCopy = () => Object.assign({}, state);
const state = {
foo: 'bar',
arr: [1, 2]
};
const shallowCopy = getStateCopy();
shallowCopy.foo = 'newFoo';
shallowCopy.arr.push(3);
console.log(state);
Either deep clone the state first instead, or use spread to add in the new properties you want:
countSeconds = () => {
this.setState({
...this.state,
secondsElapsed: this.state.secondsElapsed + 1
});
}
captureTime = (startOrStop) => {
if (startOrStop === true) {
this.setState({ ...this.state, timer: { ...this.timer, screenshotStart: this.state.secondsElapsed } });
} else if (startOrStop === false) {
const newTimingValue = this.state.secondsElapsed - this.state.timer.screenshotStart;
const newTimingArray = [...this.state.timer.timingArray, newTimingValue];
this.setState({
...this.state,
timer: {
...this.timer,
screenshotEnd: this.state.secondsElapsed,
timingArray: newTimingArray,
averageTimePerUnit: newTimingArray.reduce((a, b) => a + b, 0) / newTimingArray.length,
},
});
}
}
If captureTime is always called with either true or false, you can make things look a bit cleaner with:
captureTime = (startOrStop) => {
if (startOrStop) {
this.setState({ ...this.state, timer: { ...this.timer, screenshotStart: this.state.secondsElapsed } });
return;
}
const newTimingValue = this.state.secondsElapsed - this.state.timer.screenshotStart;
const newTimingArray = [...this.state.timer.timingArray, newTimingValue];
// etc

How to make State Hook asynchronous

I'm trying to change the value of a counter on user click.
The problem is that the state isn't synchronous, so the counter isn't updating properly and has one step late.
Here is the state:
const [state, setState] = useState({ count: 2, read: true });
const { count, read } = state;
The user first click on an element, call a reset function :
const onReset = e => {
setState({ count: 0, read: false });
getArticles(article.slug, count, count + 2);
};
Then, the user can hit a load more button which increments the counter.
const loadMore = e => {
if (read) {
getArticles(article.slug, count, count + 2)
} else {
getTagArticles(article.slug, count, count + 2);
}
setState({ ...state, count: count + 2 });
};
How can I make it asynchronous so my counter isn't late on updating the value? Should I use global variables instead of a state hook?
Thanks

componentDidMount updating synchronously

I'm loading data from saved session using:
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
this.setState({
cartItems: JSON.parse(localStorage.getItem('savedData')),
totalPrice: this.getPriceOnLoad(),
totalItems: this.getItemsOnLoad(),
});
}
}
cartItems is an array of objects. Which seems is updated before
this.getPriceOnLoad();
this.getItemsOnLoad();
functions are called, for example this.getPriceOnLoad function:
getPriceOnLoad() {
let itemsPrice = 0;
for (let i = 0; i <= this.state.cartItems.length - 1; i++) {
itemsPrice += this.state.cartItems[i].quantity * this.state.cartItems[i].price;
}
return itemsPrice;
}
but, in getPriceOnLoad function, this.state.cartItems.length is equal to 0, so for loop is not executing. I can see in React dev tools that this array has some length. Is it because componentDidMount() is executing state change synchronously and can't see updated array immediately? So my question is how could i update price and quantity of items after array is initialized?
Your state is not updated in order. For this, you could store the cartItems in a temporary value, and send it to each functions :
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems, //Short syntax for 'cartItems: cartItems'
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
}
}
You could also make your function significantly shorter by using reduce:
this.setState({
cartItems,
totalPrice: cartItems.reduce((total, item) => total + (item.quantity * item.price), 0),
totalItems: cartItems.reduce((total, item) => total + item.quantity, 0),
});
Can you show us your second function too ? It may be optimized as well. Done.
the wrong thing that you are doing is trying to use values from the state on your functions that will define your state.
you have 2 approaches to solve this:
1) use the callback function from setState and then set the state again with the new data (which into my opinion is not the best approach)
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems
}, ()=> {
this.setState({
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
})
}
}
2) send the values to your functions
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const savedCartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems,
totalPrice: this.getPriceOnLoad(savedCartItems),
totalItems: this.getItemsOnLoad(savedCartItems),
});
}
}
getPriceOnLoad() is executed before this.setState is executed. So you cannot refer to this.state in getPriceOnLoad().
When you call this.setState({}), JS first needs to generate the object for the setState() function. Means the functions you are referring to run first, then this.setState().
And in any case this.setState() is an asynchronous function, so this.state is not directly available after setState() execution.

Set state twice in a single function - ReactJS

I have a function that sets state twice, however - the second setState has to occur after 500ms since first setState has occured (animation purposes).
Code looks like:
const showAnimation = () => {
this.setState({ hidden: false });
setTimeout(() => {
this.setState({ hidden: true });
}, 500);
};
However - if I do it this way, React somehow merges these two setState's into one and my animation doesn't work as expected.
But, if I use a hack:
const showAnimation = () => {
setTimeout(() => {
this.setState({ hidden: false });
}, 0); // ------------------------------> timeout 0
setTimeout(() => {
this.setState({ hidden: true });
}, 500);
};
It works as expected. But still, I don't really like it and Im afraid that it may be some kind of a hack. Is there any better solution for such case? Thanks :)
As setState are async in React you might not get updated state immediately but setState gives you prevState param in setState function to get last updated state so you won't merge state
The syntax goes like this in your case
this.setState((prevState) => { hidden: false }, () => {
setTimeout(() => {
this.setState({ hidden: !prevState.hidden });
}, 500);
});
just update your value to the updated state using prevState
If I understand your problem correct this should work fine
Please let me know if more clarification required
If you try something like that:
const showAnimation = () => {
this.setState({ hidden: false }, () => {
setTimeout(() => {
this.setState({ hidden: true });
}, 500);
}
}
I would personally use animations within JS if you are looking to time it without setTimeout. However this may be down to the face that 'setState' is async within react.
similar :
Why is setState in reactjs Async instead of Sync?
However react does expose a callback within setState - this works for me
this.setState(
{ hidden : false },
() => {
setTimeout(()=>{this.setState({hidden : true})}, 500)
}
);
For rendering performance, react batches calls to setState such that sequential calls will be executed together and will often be reflected in the same render cycle. Per the docs:
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.
In order to ensure that the first setState has been executed prior to your second call, you can pass setState a callback as the second argument. It's not perfect, but something like the following will ensure that your second call to setState will only happen once hidden: false.
const showAnimation = () => {
this.setState({ hidden: false }, () => setTimeout(() => {
this.setState({ hidden: true });
}, 500););
};

Categories