Reputation: 947
Why does this not work as a normal one second counter?
function UseEffectBugCounter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>The count is: {count}</div>;
}
Example: https://codesandbox.io/s/sparkling-rgb-6ebcp
-Is it because of stale closures?
or
-Is it because the count is a state variable and the component would be re-rendered after the state update so a new interval will be created creating some sort of loop?
or
-Is it something else?
I'm looking for a why this occurs in this answer if possible, there are a few different articles stating why it doesn't work (as per above). But none have been able to provide a good argument so far.
Upvotes: 1
Views: 2451
Reputation: 9769
You need to pass count instead of blank array in useEffect
function UseEffectBugCounter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
},[count]);
return <div>The count is: {count}</div>;
}
Upvotes: 0
Reputation: 1325
Because you didn't add count
to useEffect's dependences, then inside the effect count always is 0
.
You should use useReducer
to resolve your problem:
function UseEffectBugCounter() {
const [count, dispatchCount] = React.useReducer((state, { type }) => {
switch(type) {
case 'inc':
return state + 1
default:
return state
}
}, 0);
React.useEffect(() => {
const intervalId = setInterval(() => {
dispatchCount({ type: 'inc'})
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>The count is: {count}</div>;
}
Upvotes: 0
Reputation: 121
If you removed second params for useEffect your application will be rendered always if u have some change in state, but this is bad practice. You need in second params choose for wich parametrs you need watch ...
Example with choosen params:
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}[count]);
Without
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
});
Upvotes: 0
Reputation: 2774
You may need to add the dependency for count
in useEffect
. Currently useEffect
is only called on the mount and is not called after that (i.e when the count value changes).
So it always says 0
because useEffect
is executed only once ( on mount ) and that time the count value is set to 0
. And thus it every time it logs 0 on setInterval
due to closure.
I have updated the code sandbox to find the reason and meaningful logs. Here is the sandbox link: https://codesandbox.io/s/admiring-thompson-uz2xe
How to find closure value: You can check the logs and traverse through prototype object to find [[Scopes]]
and you will get the values as seen in the below screenshot:
This would work:
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}, [count]);
You can check this doc: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
You can read this as well: You can read this as well: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Hope this helps!
Upvotes: 0
Reputation: 1012
You can use callback for set state to use latest counter value:
setCount(count => (count + 1));
Upvotes: 2