Why is useState within useEffect not working in React? - javascript

I use useEffect() to get a Firestore snapshot and parallel I want to count a value:
const [counter, setCounter] = useState({ mona: 0, phil: 0 });
useEffect(() => {
onSnapshot(q, (snapshop) => {
setTasks(
snapshop.docs.map((doc) => {
if (doc.data().wer === "Mona") {
console.log("Mona + 1"); // This get's executed as expected (e.g. 3 times)
setCounter({ ...counter, mona: counter.mona + 1 });
}
if (doc.data().wer === "Phil") {
console.log("Phil + 1"); // This get's executed as expected (e.g. 6 times)
setCounter({ ...counter, phil: counter.phil + 1 });
}
return {
...doc.data(),
id: doc.id,
timestamp: doc.data().timestamp?.toDate().getTime(),
};
})
);
setLoading(false);
});
}, []);
useEffect(() => {
console.log({ counter }); //this get's executed only 2 times.
}, [counter]);
When the console.log() within the map() get executed correct, why does the setCounter doesn't execute or update the counter correct?
The console.log({ counter }); btw gives nothing more than:
{counter: {mona: 0, phil: 0}}
{counter: {mona: 0, phil: 1}}

The function you pass to useEffect closes over the counter variable.
When you call setCounter it updates counter in the store, and the hook re-renders. The effect hook doesn't run again because none of the dependencies ([] - there are none) have changed.
Next time the event handler set up by onSnapshot is triggered, it uses the same value of counter as the previous time. This means that counter.phil is still 0 inside the effect hook. You add 1 to 0 again and call setCounter but this value is the same as the previous value.
Since counter hasn't changed this time, the second effect hook which depends on the value of counter doesn't get triggered.
Pass a function to setCounter to get the most recent value instead of the original closed over value:
setCounter((latestCounter) => { ...latestCounter, phil: latestCounter.phil + 1 });

React sometimes batches updates to the state. Which means all your call to setCounter only trigger one effect.
Moreover the value of counter inside your function is also updated at the end of the function, therefore you are losing updates.
What you should do:
First of all pass a callback to setCounter instead of using the value of counter. So change:
setCounter({ mona: counter.mona, phil: counter.phil + 1 });
to:
setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 }));
To force useEffect to be called multiple times you have to opt-out of batched updates using ReactDOM.flushSync:
import { flushSync } from 'react-dom';
// ...
flushSync(() => setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 })));
In this way your useEffect should be called for every single change of the counter. Obviously this is less efficient than having the updates batched.
Since you are reloading the whole dataset everytime you want to re-count everything on each call to onSnapshot instead of simply modifying the current value.
In that case you can do this:
const newCounter = { mona: 0, phil: 0};
snapshop.docs.map((doc) => {
if (doc.data().wer === "Mona") {
console.log("Mona + 1"); // This get's executed as expected (e.g. 3 times)
newCounter.mona += 1;
}
if (doc.data().wer === "Phil") {
console.log("Phil + 1"); // This get's executed as expected (e.g. 6 times)
newCounter.phil += 1;
}
// ...
});
setCounter(newCounter);
So you just compute the result and call setCounter once outside the loop with the final count. In this case you don't need to read the old state since you recompute it from scratch.
You could keep the old code and add a setCounter({mona: 0, phil: 0}) outside the loop, but I believe it would be less efficient than computing the values outside react hooks and only calling the setCounter once.

Related

Set Interval only running function once and clear interval is not working (Using react and redux)

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.

REACT - Updating state and then doing a console.log , shows unupdated state

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.

React: using state in a passed callback function to a class constructor doesnt use latest version of state [duplicate]

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;
});
}

How to count up by 1 and setState every 5 seconds in react

I have a useState variable that I want to update every 5 seconds by an increment of 1, however I want it to stop counting and start at 0 again once it reaches the array's length. Here is what I have so far. Right now the console logs are confusing, it starts to work and then the numbers just start outputting every second or faster. This works when I dont run the SetShowNum in the increment function, but that's the functionaility I need.
Any help would be greatly appreciated. Thanks
const [showNum, SetShowNum] = useState(0)
useEffect(() => {
var i = 0
var interval = setInterval(increment, 5000)
var stop = data.allSanityProducts.edges.length // right now equates to 4
function increment() {
i = i + 1
if (i === stop) {
i = 0
}
console.log(i)
SetShowNum(i)
}
})
In this solution, I added a dependency array as a second argument for the useEffect. This ensures that useEffect only runs when the component first mounts. Otherwise, the useEffect as you wrote it will keep running on each re-render so that is why you're seeing the memory leakage and all the log statements. I threw in log statements in the useEffect to observe this behavior. I would recommend running the code with and without the dependency array to observe the re-rendering occur.
The React documentation:
"If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works."
const Component = () => {
console.log("rerender");
const [showNum, SetShowNum] = useState(0);
useEffect(() => {
console.log("running useEffect");
var i = 0;
var interval = setInterval(increment, 5000);
// var stop = data.allSanityProducts.edges.length // right now equates to 4
const stop = 4;
function increment() {
i = i + 1;
if (i === stop) {
i = 0;
}
console.log(i);
SetShowNum(i);
}
}, []);
return (...)
};
Link to React useEffect docs:
https://reactjs.org/docs/hooks-effect.html
Link to a codesandbox:
https://codesandbox.io/s/nameless-sunset-1qyq4?file=/src/SO.js:0-590

ReactJS: Why different values from one state?

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.

Categories

Resources