After clicking the button the console shows 0 and the page 1
function App() {
const [count, setCount] = useState(0);
const addOne = () => {
setCount(count + 1)
console.log(count)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
I think is because the setCount() is happening asynchronously but even if I add a setTimeout to the console.log(), the console keeps showing the unupdated state
Why???
The state updation in React is always asynchronous. you will find the updated state value of count in useEffect
function App() {
const [count, setCount] = useState(0);
useEffect(()=> {
console.log('count',count);
},[count])
const addOne = () => {
setCount(count + 1)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
Closures
You are experiencing the unupdated state in the console log, because of closures.
when your function is created when the component is rendered, and closure is created with the value of count at the time the closure is created.
if the value of count is 0, and your component rerenders, a closure of your function will be created and attached to the event listener of the onlcick.
in that case, the first render of your component
const addOne = () => {
setCount(count + 1)
console.log(count)
}
is equivalent to (replace count with 0)
const addOne = () => {
setCount(0 + 1)
console.log(0)
}
therefore it makes sense in your case that count is 0 when it is console logged.
In this case, I believe its the closure you are experiencing combined with the asynchronous behavior of setState
Async behaviour
codesandbox
Async behaviour becomes a problem when asynchronous actions are occuring. setTimeout is one of the basic async actions. Async actions always require that you provide a function to the setCount function, which will accept the latest state as a parameter, with the nextState being the return value of this function. This will always ensure the current state is used to calculate the next state, regardless of when it is executed asynchronously.
const addOneAsync = () => {
setCountAsync((currentState) => {
const nextState = currentState + 1;
console.log(`nextState async ${nextState}`);
return nextState;
});
};
I have created a codesandbox demonstrating the importance of this. CLick the "Count" button fast 4 times. (or any number of times) and watch how the count result is incorrect, where the countAsync result is correct.
addOneAsync:
when the button is clicked, a closure is created around addOneAsync, but since we are using a function which accepts the currentState, when it eventually fires, the current state will be used to calculate the next state
addOne:
When the button is clicked, a closure is created around addOne where count is captured as the value at the time of the click. If you click the count button 4 times before count has increased, you will have 4 closures of addOne set to be fired, where count is captured as 0.
All 4 timeouts will fire and simply set count to 0 + 1, hence the result of 1 for the count.
Yes, you're right about the origins of this behavior and the other posters here seem to have explained how to fix it. However, I don't see the answer to your specific question:
...but even if I add a setTimeout to the console.log(), the console keeps showing the unupdated state Why???
So what you mean is that even if you handle that console.log call like so:
const addOne = () => {
setCount((count) => count + 1);
setTimeout(() => console.log(count), 1000);
}
It will STILL print the old, un-updated value of count. Why? Shouldn't the timeout allow time for count to update? I will quote the answer:
This is subtle but expected behavior. When setTimeout is scheduled it's using the value of count at the time it was scheduled. It's relying on a closure to access count asynchronously. When the component re-renders a new closure is created but that doesn't change the value that was initially closed over.
Source: https://github.com/facebook/react/issues/14010#issuecomment-433788147
So there you have it.
Related
Take the below code as an example.
import { useEffect, useState } from "react";
export default function Stopwatch(){
const [stopwatch, setStopwatch] = useState(0);
function updateStopwatch(){
console.log("Updating stopwatch from ", stopwatch, " to ", stopwatch+1);
setStopwatch(stopwatch + 1);
}
useEffect(() => {
const interval = setInterval(updateStopwatch, 1000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<h1>{stopwatch}</h1>
</div>
);
}
In this code, the function updateStopwatch will be invoked periodically without depending on any re-rendering of the component ( Although its invocation will cause a re-render ) and each time it will be invoked, it needs to retrieve the current up-to-date value of the state variable stopwatch, so it can update it accordingly. So that the stop watch will keep counting up.
However, what happens is that it only gets the value of stopwatch at the moment the function is declared. It's not really subscribed to a state variable but more like it just reads a constant once. And as a result the count show 1 and is stuck there.
So how can I make such a function get an up-to-date value of a state variable whenever it's invoked?
In your setState method, you can retrieve a prevState argument, which is always the right value, here is the way you could do your function based on your example :
function updateStopwatch(){
setStopwatch((prevState) => prevState + 1);
}
This is an issue of a stale closure over the initial stopwatch state value. Use a functional state update to correctly update from the previous state value instead of whatever value is closed over in callback scope at the time of execution. Move updateStopwatch declaration into the useEffect hook to remove it as an external dependency.
Example:
export default function Stopwatch(){
const [stopwatch, setStopwatch] = useState(0);
useEffect(() => {
function updateStopwatch(){
setStopwatch(stopwatch => stopwatch + 1);
}
const interval = setInterval(updateStopwatch, 1000);
return () => clearInterval(interval);
}, []);
return (
<div>
<h1>{stopwatch}</h1>
</div>
);
}
I am trying to run a function continuously until a condition is met, but in this test case, until the off button is pressed. My first issue is that the function does not stop when i press the off button.
let intervalId
function on(){
intervalId = window.setInterval(function(){
setnum(num=>num+1)
//setnum(num + 1)
//Line 11 results in the number going up once and if i keep pressing the button it goes up by one but flashes between numbers more and more frantically every press. The off button has no effect.
//updateUserMoney()
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
{num}
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
The second issue is that the function I want to run in the interval (that setnum is standing in for) is actually
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
Here, the incrementTime function works as intended and continues to increment, but the update user function only fires once.
I think it has the same problem that line 11 has where setnum(num + 1) doesn't work but setnum(num => num + 1) does. I haven't used the second syntax much and don't understand why it's different can anybody tell me?
Here's the full code
import { useState } from "react";
import { batch, useDispatch, useSelector } from "react-redux";
import { incrementTime, updateUser } from "../actions";
const GeneralActions = () => {
const dispatch = useDispatch()
const user = useSelector((state)=>state.user)
const [num, setnum]= useState(0)
let intervalId
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
function on(){
intervalId = window.setInterval(function(){
updateuserMoney()
setnum(num=>num+1)
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
<br/>
<>{num}</>
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
);
}
export default GeneralActions;
Any insight is appreciated. Thank you!
Every time you set a new state value in React, your component will rerender. When your GeneralActions component rerenders, your entire function runs again:
const GeneralActions = () => {
// code in here runs each render
}
This means things such as intervalId, will be set to undefined when it runs let intervalId; again, and so on this particular render you lose the reference for whatever you may have set it to in the previous render. As a result, when you call off(), it won't be able to refer to the intervalId that you set in your previous render, and so the interval won't be cleared. If you want persistent variables (that aren't related to state), you can use the useRef() hook like so:
const GeneralActions = () => {
const intervalIdRef = useRef();
...
function on(){
clearInterval(intervalIdRef.current); // clear any currently running intervals
intervalIdRef.current = setInterval(function(){
...
}, 400);
}
function off(){
clearInterval(intervalIdRef.current);
}
}
One thing that I've added above is to clear any already created intervals when on() is executed, that way you won't queue multiple. You should also call off() when your component unmounts so that you don't try and update your state when the component no longer exists. This can be done using useEffect() with an empty dependency array:
useEffect(() => {
return () => off();
}, []);
That should sort out the issue relating to being unable to clear your timer.
Your other issue is with regards to setNum() is that you have a closure over the num variable for the setTimeout() callback. As mentioned above, every time your component re-renders, your function body is executed, so all of the variables/functions are declared again, in essence creating different "versions" of the num state each render. The problem you're facing is that when you call setInterval(function(){}), the num variable within function() {} will refer to the num version at the time the function was created/when setInterval() function was called. This means that as you update the num state, your component re-renders, and creates a new num version with the updated value, but the function that you've passed to setInterval() still refers to the old num. So setNum(num + 1) will always add 1 to the old number value. However, when you use useState(num => num + 1), the num variable that you're referring to isn't the num "version"/variable from the surrounding scope of the function you defined, but rather, it is the most up to date version of the num state, which allows you to update num correctly.
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
Sorry for the confusing title, but here is what is going on:
In MyComponent, I am setting a count state with the useState React hook.
Once the component mounts (ie. useEffect with no dependencies), I am instantiaitng two MyClass objects with the first argument as a callback function that increments the state, and the second argument is the timeOut period to call the callback function.
The first instance of MyClass calls the callback in 1000 ms and sets the new value for count, which once updated, is logged in the second useEffect.
However, when the second instance of MyClass calls the call back (after timeOut period of 3000 ms), and tries incrementing the count value, it uses the state of count from when the MyClass was instantiated (which was 0), so it increments count to 1 (wanted behavior is to increment to 2, since the first instance of MyClass already incremented count from 0 to 1)
This is not an issue related to the asynchronicity behavior of setState because it is evident that that first update to count happens before the second instance tries to update it again (the second useEffect gets called when count state is updated, which from the console log messages you can see is happening before second instance of MyClass calls the call back).
JSFiddle link: https://jsfiddle.net/hfv24dpL/
So in conclusion, I think that the issue is that the count state in the callback function is a copy of the count state at the time when the callback functions were passed to the MyClass constructor.
A solution to this example could be to just instantiate the second instance of MyClass when the count state is updated (in the second useEffect), but this is not the solution I am looking for.
Another solution is to use setCount(prevCount => prevCount + 1) to increment count, but this isnt viable in my real application (MyComponent and MyClass are a skeleton example of my real React application that I wrote just for this question).
I want to be able to instantiate the classes togethor when component mounts (in first useEffect), and have the callbacks refer to the most updated version of count.
Is there a solution for this ^ or is there no way around this javascript and React implementation? Thanks for reading all this, I know its long :)
import React, { useState, useEffect } from 'react';
class MyClass{
constructor(callback, timeOut){
// call callback in timeOut milliseconds
this.timeOutId = setTimeout(() => {
callback();
}, timeOut)
}
clearTimeOut(){
clearTimeout(this.timeOutId);
}
}
function MyComponent(){
var [count, setCount] = useState(0);
// component did mount
useEffect(() => {
let myClass1 = new MyClass(funcToCallback, 1000);
let myClass2 = new MyClass(funcToCallback, 3000);
// when component unmounts, clear the timeouts of MyClass instances
return () => {
myClass1.clearTimeOut();
myClass2.clearTimeOut();
}
}, []);
// counter state updated
useEffect(() => {
console.log("COUNT UPDATED TO: ", count);
}, [count])
// get counter and increment it by 1
function funcToCallback(){
console.log("CALLBACK CALLED");
let newCount = count + 1;
incCount(newCount);
}
function incCount(newCount){
console.log("NEW COUNT: ", newCount);
setCount(newCount);
}
return (
<div>
COUNT: { count }
</div>
)
}
The funcToCallback that gets used is the one in the initial mount of the component, when count is 0. Variables declared with const don't change, and the useEffect callback only gets called once, the count that the funcToCallback closes over remains at 0 forever.
Easiest fix would be to use the function version of the setter instead, which will give you the prior state as an argument - then you can just increment it. Change
function incCount(newCount){
console.log("NEW COUNT: ", newCount);
setCount(newCount);
}
to
function incCount(){
setCount((lastCount) => {
console.log("NEW COUNT: ", (lastCount + 1));
return lastCount + 1;
});
}
I am currently refactoring a react app from setState to hooks. I can't understand why the state variables aren't changed. Here is an example:
import React, { useState, useEffect } from 'react';
function Hook() {
const [num, setNum] = useState(1);
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, []);
const logNum = () => {
console.log(num);
}
const handleToggle = () => {
if (num == 1) {
console.log('setting num to 2');
setNum(2);
} else {
console.log('setting num to 1');
setNum(1);
}
}
return (
<div>
<button onClick={handleToggle}>TOGGLE BOOL</button>
</div>
);
}
export default Hook;
When i click the button, I was expecting the output to be something like:
// 1
// setting num to 2
// 2
// setting num to 1
// 1
But the output look like this:
Why is the updated num variable not logged?
Shouldn't the logNum() function always point to the current value of the state?
That's why effect dependencies have to be exhaustive. Don't lie about dependencies.
logNum closures over num, so on every rerender there is a new num variable containing the new value, and a new logNum function logging that value. Your effect however gets initialized only once, thus it only knows the first logNum. Therefore, you have to add logNum as a dependency, so that the effect gets updated whenever num and thus logNum changes:
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, [logNum]);
You'll notice that your effect does not correctly clean up, you should add a removeEventListener too.
return () => window.removeEventListener("mousemove", logNum);
Now if you debug this piece of code, you'll notice that the effect triggers on every rerender. That is because a new logNum function gets created on every rerender, no matter wether num changes or not. To prevent that, you can use useCallback to make the logNum reference stable:
const logNum = useCallback(() => console.log(num), [num]);
An alternative to all of this would be to use a reference to the current state:
const actualNum = useRef(num);
// that works no matter when and how this is executed
console.log(actualNum.current);
So I'm getting into Reactjs with very basic component.
I'm logging out the same state from different functions, but what I'm seeing is the different values.
import React, { useState, useEffect, useRef } from "react";
const Test = props => {
const [count, setCount] = useState(0);
useEffect(()=>{
setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
},[props]);
function btnClick() {
const newCount = count + 1;
setCount(newCount);
console.log("count changed to: ", newCount);
}
return (
<div>
count is {count}
<br></br>
<button onClick={btnClick}>+</button>
</div>
);
};
export default Test;
Output after some clicks and wait, log is:
Test.js:8 count in interval is: 0
Test.js:15 count changed to: 1
Test.js:15 count changed to: 2
Test.js:15 count changed to: 3
Test.js:15 count changed to: 4
(8 rows) Test.js:8 count in interval is: 0
I expect the "count" to be the same in both functions.
Can any one explain this?
Thank so much.
Test only has one setInterval function where count is always 0. Since it's only created during initial render.
It never had another setInterval created because the effect never got triggered with [props] as the dependency.
To have setInterval's count change on every re-render:
Remove the dependency
Return a clean-up function inside the effect
useEffect(
() => {
const t = setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
return () => clearInterval(t); // cleanup on every re-render
}
// no dependency: effect runs on every re-render
);
But the above code will have a warning:
"missing count dependency"
So simply add count as dependency to only run the effect when count changes.
useEffect(
() => {
const t = setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
return () => clearInterval(t); // cleanup "old" setInterval
}
, [count] // ony run effect every time count changes
);
The value of count doesn't change, this is the expected behavior, though not an obvious one.
See, you even declare count as a const count, it is immutable. What is happening instead is that during the first render count gets assigned value of 0. The value of count never changes, what happens instead is that component Test is called each time you change the state, and function useState assigns different values to the constant count, which is new constant every time.
So during the first render the value of const count gets captured by closure inside your function that is called by setInterval and the value stays 0 forever.