Consider this code:
const [seconds, setSeconds] = useState<number>(START_VALUE);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((previousSeconds) => previousSeconds - 1);
}, 1000);
if (seconds <= 0) {
clearInterval(intervalId);
functionX();
}
return () => clearInterval(intervalId);
}, [seconds]);
The problem with the above code is that every second the useEffect
gets triggered, is there any way to access the value out of setSeconds
calculation to be used inside setInterval
?
The problem with the above code is that every second the
useEffect
gets triggered
Your useEffect
hook is trying to do too much. You can split the code/logic up into as many logical effect as is necessary for your use case. In any case you will have at least 1 useEffect
hook being called each time the seconds
state updates so the code can correctly check when it reaches 0.
is there any way to access the value out of
setSeconds
calculation to be used insidesetInterval
?
No, not really. In doing so you will break a few design patterns.
setInterval
callback creates a Javascript closure over lexical values in scope when it is created, so the callback can't see the updated seconds
state value when it is called.setSeconds
callback, but since React state updater functions are to be considered pure functions, you can't call functionX
or cancel the interval timer from that scope.Trivially you have at least a couple of options:
useEffect
Hooks to Control Interval TimerUse one useEffect
hook with empty dependency array to initiate the interval and clean up any running intervals in the event of component unmounting, and another useEffect
hook to handle the side-effect of checking when the timer expires and invoking functionX
. Use a React ref to hold a reference to the timer id.
const [seconds, setSeconds] = useState<number>(START_VALUE);
const timerRef = useRef<number | null>(null);
useEffect(() => {
timerRef.current = setInterval(() => {
setSeconds((seconds) => seconds - 1);
}, 1000);
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
useEffect(() => {
if (timerRef.current && seconds <= 0) {
clearInterval(timerRef.current);
timerRef.current = null;
functionX();
}
}, [seconds]);
useEffect
Hook to Control Timeout TimerUse a single useEffect
hook and a setTimeout
timer instead of an interval, so the effect running triggers the next timer iteration. This avoids creating extraneous unnecessary intervals your original code was doing.
const [seconds, setSeconds] = useState<number>(START_VALUE);
const functionX = () => console.log("timer expired");
useEffect(() => {
const intervalId = setTimeout(() => {
setSeconds((seconds) => seconds - 1);
}, 1000);
if (seconds <= 0) {
clearTimeout(intervalId);
functionX();
}
return () => {
clearTimeout(intervalId);
}
}, [seconds]);