I am trying to start and stop a timer with a recursive setTimeout
, since setInterval
doesn't fit my need. When the user presses start, the timer starts counting.
1...2...3...4..5...1...2...3..4...5...1..2...3..4...5...
And when the user presses stop, this Stops happening. If I press start again, changing the state of the button, the counter does the same thing again.
I thought the way to do it would be this.
const ref = useRef(null);
useEffect(() => {
if (!stream) {
ref.current=setTimeout(repeatingFunc, 1000);
} else {
clearTimeout(ref.current);
}
}, [stream]);
function repeatingFunc() {
console.log("It's been 5 seconds. Execute the function again.");
setTimeout(repeatingFunc, 5000);
}
What am I doing wrong?
The timer doesn't stop. It keeps going even if I press stop! (when buttonState
is false
)
CodePudding user response:
The reason why the timer doesn't stop is because you only store the timeout ID of the initial setTimeout
call. When repeatingFunc
is called another setTimeout
callback is registered, which has a new timeout ID.
So when you try to clear the timeout using clearTimeout(ref.current)
you are passing an outdated timeout ID.
Since the previous setTimeout
registration is already called, you no longer need to store the previous ID and you could simply replace it with the new one.
const ref = useRef(null);
useEffect(() => {
if (!stream) {
ref.current = setTimeout(repeatingFunc, 1000);
} else {
clearTimeout(ref.current);
}
}, [stream]);
function repeatingFunc() {
console.log("It's been 5 seconds. Execute the function again.");
ref.current = setTimeout(repeatingFunc, 5000);
// ^^^^^^^^^^^^ update timeout ID after registering a new timeout
}
Note that the scope of repeatingFunc
is fixed in a point in time where you initially call setTimeout(repeatingFunc, 1000)
, meaning that things like states and properties can contain outdated values. This might or might not become a problem depending on your context.
You might also want to add a useEffect
callback that clears the timeout on component dismount. Otherwise your loop keeps going, even when the component is no longer mounted.
useEffect(() => {
// ...
}, [streem]);
// clear timeout on component dismount
useEffect(() => () => {
clearTimeout(ref.current);
}, []);
function repeatingFunc() {
// ...
}
CodePudding user response:
You can define a custom timer like this:
function CustomTimer(func) {
this.id = -1;
this.func = func;
this.start = function (interval) {
this.stop();
if (this.func) {
var t = this;
this.id = setTimeout(function () { t.func.call(t) }, interval || 1000);
}
}
this.stop = function () {
if (this.id >= 0) {
clearTimeout(this.id);
this.id = -1;
}
}
}
And use it:
var myTimer = new CustomTimer(function () {
console.log("It's been 5 seconds. Execute the function again.");
this.start(5000);
});
myTimer.start(5000);
You can change the function to execute in any moment and also the interval. For example: run a start event in 1 second and then, 5 times every 5 seconds do other thing:
var secondFunc = function () {
console.log("It's been 5 seconds. Execute the function again.");
if ( this.count < 5)
this.start(5000);
}
var myTimer = new CustomTimer(function () {
console.log("First event at 1 second");
this.count = 0;
this.func = secondFunc;
this.start(5000);
});
myTimer.start(1000);