variable in useState not updating in useEffect callback
Solution 1
There are a couple of issues:
- You're not returning a function from
useEffect
to clear the interval - Your
inc
value is out of sync because you're not using the previous value ofinc
.
One option:
const counter = ({ count, speed }) => {
const [inc, setInc] = useState(0);
useEffect(() => {
const counterInterval = setInterval(() => {
setInc(inc => {
if(inc < count){
return inc + 1;
}else{
// Make sure to clear the interval in the else case, or
// it will keep running (even though you don't see it)
clearInterval(counterInterval);
return inc;
}
});
}, speed);
// Clear the interval every time `useEffect` runs
return () => clearInterval(counterInterval);
}, [count, speed]);
return inc;
}
Another option is to include inc
in the deps array, this makes things simpler since you don't need to use the previous inc
inside setInc
:
const counter = ({ count, speed }) => {
const [inc, setInc] = useState(0);
useEffect(() => {
const counterInterval = setInterval(() => {
if(inc < count){
return setInc(inc + 1);
}else{
// Make sure to clear your interval in the else case,
// or it will keep running (even though you don't see it)
clearInterval(counterInterval);
}
}, speed);
// Clear the interval every time `useEffect` runs
return () => clearInterval(counterInterval);
}, [count, speed, inc]);
return inc;
}
There's even a third way that's even simpler:
Include inc
in the deps array and if inc >= count
, return early before calling setInterval
:
const [inc, setInc] = useState(0);
useEffect(() => {
if (inc >= count) return;
const counterInterval = setInterval(() => {
setInc(inc + 1);
}, speed);
return () => clearInterval(counterInterval);
}, [count, speed, inc]);
return inc;
Solution 2
The issue here is that the callback from clearInterval
is defined every time useEffect
runs, which is when count
updates. The value inc
had when defined is the one that will be read in the callback.
This edit has a different approach. We include a ref to keep track of inc
being less than count
, if it is less we can continue incrementing inc
. If it is not, then we clear the counter (as you had in the question). Every time inc
updates, we evaluate if it is still lesser than count and save it in the ref
. This value is then used in the previous useEffect
.
I included a dependency to speed
as @DennisVash correctly indicates in his answer.
const useCounter = ({ count, speed }) => {
const [inc, setInc] = useState(0);
const inc_lt_count = useRef(inc < count);
useEffect(() => {
const counterInterval = setInterval(() => {
if (inc_lt_count.current) {
setInc(inc => inc + 1);
} else {
clearInterval(counterInterval);
}
}, speed);
return () => clearInterval(counterInterval);
}, [count, speed]);
useEffect(() => {
if (inc < count) {
inc_lt_count.current = true;
} else {
inc_lt_count.current = false;
}
}, [inc, count]);
return inc;
};
Comments
-
Abhay Sehgal almost 2 years
I'm having an issue while using useState and useEffect hooks
import { useState, useEffect } from "react"; const counter = ({ count, speed }) => { const [inc, setInc] = useState(0); useEffect(() => { const counterInterval = setInterval(() => { if(inc < count){ setInc(inc + 1); }else{ clearInterval(counterInterval); } }, speed); }, [count]); return inc; } export default counter;
Above code is a counter component, it takes count in props, then initializes inc with 0 and increments it till it becomes equal to count
The issue is I'm not getting the updated value of inc in useEffect's and setInterval's callback every time I'm getting 0, so it renders inc as 1 and setInterval never get clear. I think inc must be in closure of use useEffect's and setInterval's callback so I must get the update inc there, So maybe it's a bug?
I can't pass inc in dependency ( which is suggested in other similar questions ) because in my case, I've setInterval in useEffect so passing inc in dependency array is causing an infinite loop
I have a working solution using a stateful component, but I want to achieve this using functional component
-
Alvaro over 4 years@DennisVash Thanks for letting me know, could you please expand on why it won't work?
-
Dennis Vash over 4 yearsDid you check it? There are plenty of reasons, the main one is that increasing reference won't cause re-render
-
Abhay Sehgal over 4 yearsActually, In Parent Component (where I'm using Counter) value of count is coming from the server that's why I've added it in dependency array, I can remove its dependency by mounting Counter component after API call. So useEffect will work as componentDidMount. Now using useRef is not going to rerender component. So i need state to rerender component
-
Abhay Sehgal over 4 yearsinc.current value is getting update, but react is not rerendering it, as it's not in state
-
Alvaro over 4 years@AbhaySehgal this is what I meant with "side-effects". So you need
inc
inside a state. Ok, let me edit. -
Clarity over 4 yearsThis might work for this particular use case but will cause bugs if the component re-renders too often: overreacted.io/making-setinterval-declarative-with-react-hooks
-
Alvaro over 4 years@AbhaySehgal I included a different approach.
-
Dennis Vash over 4 yearsCan you reproduce any bug?
-
Abhay Sehgal over 4 yearstry console.log() in setInc function, it's calling infinitely
-
Dennis Vash over 4 yearsIt will run until unmount, I guess you want to clear interval on condition, let me fix it
-
Dennis Vash over 4 years@AbhaySehgal I edited the answer, now it clears on count condition
-
Abhay Sehgal over 4 yearsoption one : else case is returning inc then clearing setInterval so that line will never get executed and will cause infinite loop
-
Abhay Sehgal over 4 years@Alvaro I tried your solution, It works perfectly, but there is one issue if you pass speed as 0 ( which maybe never gonna happen ) then inc gets increment to (count + 1) may be due to async nature of setInterval
-
Alvaro over 4 years@AbhaySehgal. Im glad it helped. As far as I understand,
setInterval
will always run, if its0
then it will only run one time. I have a doubt which shouldnt affect this case, but its something to consider and test out. If the interval time is shorter than the time the component takes to render, it might run more than 1 time before the state updates. But Im not really sure this would be the case. -
Abhay Sehgal over 4 yearsAll your options are working perfectly, thanks for your answer, there is one issue in second option if should return setInc(inc + 1)