Home > Software design >  Start and Stop a Timer with Recursive setTimeout
Start and Stop a Timer with Recursive setTimeout

Time:04-14

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);
  • Related