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
?
3
Answers
The below post recommends a Ref
setInterval call is open in the useEffect. It gets invoked for every change in the state. Therefore it goes infinite. This is the issue with the current code.
Please put a conditional for setInterval so that it gets invoked only once. Since it is a scheduling code, it should get invoked only once. Once it gets scheduled, it works automatically on the schedules.
In order to put a conditional for setInterval, please use a ref as shown in the sample code below. As you know, Ref will retain its value during renders. Therefore it would be an ideal choice here.
For more about its usage, please see here When to use refs
A clean up code for useEffect is not required in this case, since the custom code inside useEffect takes care of it. It clears the interval set.
App.js
Test run
The component renders 4 times in total.
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 1useEffect
hook being called each time theseconds
state updates so the code can correctly check when it reaches 0.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 updatedseconds
state value when it is called.setSeconds
callback, but since React state updater functions are to be considered pure functions, you can’t callfunctionX
or cancel the interval timer from that scope.Trivially you have at least a couple of options:
Use Two
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 anotheruseEffect
hook to handle the side-effect of checking when the timer expires and invokingfunctionX
. Use a React ref to hold a reference to the timer id.Use One
useEffect
Hook to Control Timeout TimerUse a single
useEffect
hook and asetTimeout
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.This post recommends setTimeout instead of setInterval
The reasoning is this : useEffect and a state setter invocation within it itself will automatically forms a loop. In fact, it is an infinite loop unless an exit is provided. Therefore developing another loop through setInterval may be a redundant code. The point is we need to place an exit suiting to the logic. The sample code below does the same.
Notes: The setTimeout over here invokes the state setter one every second. Since the effect is depending on the state, the previous change will itself invoke another setTimeout. The same will repeat until the exit condition meets.
App.js
Test run
The component renders 4 times in total.