I want to be able to call setInterval (or something similar) at two different lengths, alternating.
For example, running a function after 5 seconds, then 1 second, then 5 seconds again, and so on.
Is this possible? I tried a function that alternates the value, but it didn't seem to work.
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num = 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
JS timers have a very complicated history.
Using a recursive setTimeout invocation is a simple and elegant solution as long (as your runtime implements tail call optimization).
Separate from the issue of recursion is the issue of timer drift. This is covered in the YouTube video JavaScript counters the hard way - HTTP 203 if you'd like an accessible introduction.
In many JS engines (e.g. V8) setInterval will handle drift correction for you, so there's actually an advantage to using it over recursively invoking setTimeout. (Check the millisecond timestamps in the console messages in the snippet below to verify this.)
In order to determine the constant interval argument you'll need for setInterval, you'll need to find the greatest common factor of your delay durations. Once you have this value, you can use it as the base interval delay, and keep track of your interval state to determine whether you should switch to the next interval delay, run your other code, etc. Here's a minimal example:
const durations = [1000, 5000];
// If you can't determine this in advance and use a constant value,
// then you can calculate it at runtime using a function:
const gcf = 1000; // or const gcf = findGreatestCommonFactor(durations);
let durationIndex = 0;
let elapsed = 0;
function update () {
elapsed += gcf;
const ready = elapsed === durations[durationIndex];
if (ready) {
elapsed = 0;
durationIndex = (durationIndex + 1) % durations.length;
}
return ready;
}
setInterval(() => {
const ready = update();
if (!ready) return;
// Do your interval task, for example:
console.log('tick');
}, gcf);
The problem with setInterval() is that the time is taken into account just once. You can use setTimeout() with recursion instead:
function doAction(flipFlop) {
setTimeout(() => {
console.log(flipFlop ? 'flip' : 'flop');
doAction(!flipFlop);
// do some other action...
}, flipFlop ? 1000 : 3000);
}
doAction(true);
Watch out though if you have a long running process, this recursion gets deeper and deeper.
I think this method is the easiest:
setInterval(() => {
console.log("first");
setTimeout(() => console.log("second"), 750);
}, 2000);
This creates an interval that alternates between 1250 and 750 milliseconds.
The problem with your code
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num === 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
The last few lines (the setInterval) call are only getting called once with the initial value of num and thus any future changes to num won't be reflected in the setTimeout call.
How to fix it
You should use setTimeout within the function that has your code and call your function recursively:
const doStuff = (time = 1000) => {
// Your code here
// generate the next time to wait
const nextTime = time === 5000 ? 1000 : 5000;
// call the function again after waiting `time` milliseconds
setInterval(() => doStuff(nextTime), time);
}
Then you would call doStuff to start it. If you wanted to start it immediately with the next one happening after 1 second you could do:
doStuff();
Or if you wanted to call it after 5 seconds with the next one happening one second after that:
setTimeout(doStuff, 5000);
The difference here compared to your code is that the variable that represents the time is being used over and over again as it changes instead of just once on initial code execution.
I was wondering if there is a nicer object oriented way of creating this timer? (without global vars!)
let secondsPassed = 0;
let timerId;
function startTimer() {
clearInterval(timerId);
timerId = setInterval(function() {
const seconds = twoDigits((Math.floor(secondsPassed )) % 60);
const minutes = twoDigits(Math.floor(secondsPassed / 60) % 60);
const hours = Math.floor(secondsPassed / 60 / 60);
$('#timer').text(`${hours}:${minutes}:${seconds}`);
secondsPassed++;
}, 1000);
$(window).blur(function() {
clearInterval(timerId) // stop timer when user leaves tab
});
$(window).focus(function() {
startTimer(); // continue timer when user comes back
});
}
Your current implementation is actually wrong. Every time you call startTimer, it installs startTimer as a new window focus event handler, leading to multiple started intervals when you focus the window the second time; growing exponentially. The onfocus handler should only run the timerId = setInterval(…) line - put that in a nested helper function to call only that.
This also makes it unnecessary to declare the variables globally.
function createTimer() {
let secondsPassed = 0;
let timerId;
function resume() {
if (timerId) return; // prevent multiple intervals running at the same time
timerId = setInterval(() => {
const seconds = twoDigits((Math.floor(secondsPassed )) % 60);
const minutes = twoDigits(Math.floor(secondsPassed / 60) % 60);
const hours = Math.floor(secondsPassed / 60 / 60);
$('#timer').text(`${hours}:${minutes}:${seconds}`);
secondsPassed++;
}, 1000);
}
function pause() {
clearInterval(timerId);
timerId = undefined;
}
$(window).blur(pause); // stop timer when user leaves tab
$(window).focus(resume); // continue timer when user comes back
resume(); // now start the timer
}
Now how to make that object-oriented? Just return an object from createTimer. Put resume and pause as methods on that object. Maybe add some more methods for starting, stopping, resetting, whatever you need. Maybe use a property on the object instead of the secondsPassed local variable. Or expose the local variable using a getter.
And to make it reusable, of course you can make createTimer accept arguments, from the selector of the output element, to the output element itself, to a callback function that will be called with the current time on every tick.
Edit: With this answer, you have to implement the Timer class yourself first. The code only shows how you could name the methods of the timer, how you create the instance and call its functions. The timer should (principle "separation of concerns") only handle the counting and provide the functionalities needed, like starting and stopping.
If you want to have an OOP solution for your timer, you shouldn't let the Timer class know the ID of the DOM container (like one of your comments to your question suggested).
You should read into the topic using this:
https://appdividend.com/2019/05/22/javascript-class-example-how-to-use-class-in-javascript-tutorial/
Let us assume, that you already implemented the class. Your code above should look like the following:
// Create own scope for the function, so that variable are not assigned to windows-object.
(function() {
let secondsPassed = 0;
let timer = new Timer();
// events, if necessary
timer.onTick((seconds) => { secondsPassed = seconds });
timer.onStop(() => { secondsPassed = 0; })
// Called by a button
function startTimer() {
timer.start();
}
// Example: Display alert with current timer seconds on click
function displaySecondsOfTimer() {
alert(timer.getSeconds());
}
$(window).blur(function() {
timer.stop(); // stop timer when user leaves tab
});
$(window).focus(function() {
timer.start(); // continue timer when user comes back
});
})();
So I think, you have a good example to code your first Timer class in native JavaScript! :)
I'm trying to create a function that starts and stops a timer. The starting is always on the click of a button, but the stopping can be due to the timer running down or the function being called again from another function.
This is what I have so far. Works perfect for what you see but I cannot figure out how to incorporate clearInterval() so that it stops when the game is won. The functioning calling timerCountdown is located in a different js file. I've read answers to similar questions but they are all seem to be doing it a little differently to where I can't make it work for my case
I do realize that I need to call clearInterval(count) but I don't know how to incorporate this into the function itself.
const timerCountdown = () => {
let count = setInterval(() => {
let timeLeft = timer.innerHTML
if (timeLeft > 0) {
timer.innerHTML = timeLeft - 1
} else {
gameOverMessage()
}
}, 1000)
}
You need to push the interval id in a global variable. Like that you can use another function to stop the interval when you want.
Like
let intervalId; // define `count` globaly
let timer = document.getElementById('timer')
const timerStop = () => {
clearInterval(intervalId)
}
const timerRestart = () => {
timerStop()
timer.innerHTML = 100;
}
const timerStart = () => {
timerStop(); // Stop timer by security, e.g. if you call `timerStart()` multiple times
intervalId = setInterval(() => {
let timeLeft = timer.innerHTML;
if (+timeLeft > 0) {
timer.innerHTML = +timeLeft - 1
} else {
timerRestart();
gameOverMessage()
}
}, 1000)
}
<div id="timer">100</div>
<div>
<button onclick="timerStart()">Start</button>
<button onclick="timerStop()">Pause</button>
<button onclick="timerRestart()">Restart</button>
</div>
setInterval makes a best effort to space the running of the callback according to the interval you specify. The thing is: in a game, what you actually want is the current state of the world to be printed to the screen in smooth and timely fashion. This is different to the behavior of setInterval, which knows nothing about the screen and is blindly called repeatedly.
For example: if you kick-off setInterval(foo, 100) for your game in a browser tab, and then navigate to another tab in your browser, wait ten seconds and then return to your game, your foo callback will be invoked about a hundred times in rapid succession as the queued callbacks are "drained". It is highly unlikely you want this behavior.
requestAnimationFrame is a better solution for this, because it is only called when (shortly before) your game is rendered - which is what you want.
In the following code a timer object is created by createTimer. The timer has start, stop and toggle methods.
The start method records when it was invoked and triggers requestAnimationFrame, supplying a callback called tick. Every time a tick occurs, we run some logic to see which (if any) callback to invoke.
If the time elapsed is greater than or equal to the duration of the timer, then the onTimeout callback is invoked and the timer is stopped.
If the time elapsed is smaller than the duration, but greater than or equal to the interval period, then we update the lastInterval and invoke the onInterval callback.
Otherwise we simply cue up another tick of the timer.
The stop method simply uses the request animation ID to cancel the timer with cancelAnimationFrame.
function createTimer() {
let rafId = null
function start({duration = 10000, interval = 1000, onInterval, onTimeout, onStop, startTime=performance.now(), lastInterval = startTime}) {
function tick(now=performance.now()) {
const elapsed = now - startTime
if (elapsed >= duration) {
cancelAnimationFrame(rafId)
rafId = null
return onTimeout()
}
if ((now - lastInterval) >= interval) {
lastInterval = now
onInterval({
duration,
elapsed
})
}
rafId = requestAnimationFrame(tick)
}
rafId = requestAnimationFrame(tick)
}
function stop() {
cancelAnimationFrame(rafId)
rafId = null
return onStop()
}
function toggle(...args) {
rafId ? stop() : start(...args)
}
const timer = {
start,
stop,
toggle
}
return timer
}
const timer = createTimer()
const onInterval = ({duration, elapsed})=>console.log(`Remaining: ${((duration - elapsed)/1000).toFixed(0)}`)
const onTimeout = ()=>console.log('Timed out.')
const onStop = ()=>console.log('Manually stopped.')
document.getElementById('btn').addEventListener('click', () => timer.toggle({
onInterval,
onTimeout,
onStop
}))
<button id="btn">Toggle Timer</button>
You could take a global variable intervalId and clear the interval timer, if won or if no time is avaliable.
var intervalId;
const timerCountdown = () => {
intervalId = setInterval(() => {
let timeLeft = timer.innerHTML
if (timeLeft > 0) {
timer.innerHTML = timeLeft - 1
} else {
clearInterval(intervalId);
gameOverMessage();
}
}, 1000)
},
won = () => {
clearInterval(intervalId);
// additional code
};
My setTimeout() function works, but my clearTimeout() is not working. Even though I have an 'if' statement that's supposed to run the clearTimeout function once my variable 'secs' is less than 0, the timer keeps counting down into negative numbers. When I type my variable name, 'secs' into the console, I get undefined, even though it's defined as a parameter in the function called by my setTimeout. I don't know what I'm doing wrong. Can anyone help, please?
My full code is at https://codepen.io/Rburrage/pen/qBEjXmx;
Here's the JavaScript snippet:
function startTimer(secs, elem) {
t = $(elem);
t.innerHTML = "00:" + secs;
if(secs<0) {
clearTimeout(countDown);
}
secs--;
//recurring function
countDown = setTimeout('startTimer('+secs+',"'+elem+'")', 1000);
}
Add a condition to call recursive function like below.
if (secs < 0) {
secs = secsInput;
}
//recurring function
countDown = setTimeout('startTimer('+secs+',"'+elem+'")', 1000);
For a countdown timer, I would recommend using setInterval and clearInterval instead. setInterval will repeatedly run the callback function for you. It might look like this:
let countdown;
function startTimer(secs, elem) {
countdown = setInterval(function(){
t = $(elem);
t.innerHTML = "00:" + secs;
secs--
if (secs < 0) {
clearInterval(countdown);
}
}, 1000);
}
By the time you call clearTimeout(countDown), countDown refers to the previous timeout, that already timed out. It will not stop the one yet to start. You could just not re set the timeout, like
if(!/*finished*/) setTimeout(startTimer, 1000, secs, elem);
In your case, it's more convenient to use setInterval and clearInterval.
To keep the setTimeout and clearTimeout functions, you should add return in the if statement.
function startTimer(secs, elem) {
t = $(elem);
t.innerHTML = "00:" + secs;
if(secs<0) {
clearTimeout(countDown);
return;
}
secs--;
//recurring function
countDown = setTimeout('startTimer('+secs+',"'+elem+'")', 1000);
}
So there are 4 events in my opinion that will have to be addressed by the timer:
The quiz starts
The quiz ends
The timer runs out
The player answers a question
This can be solved by a function returning an object with some options.
The createTimer can be used to set the parameters for the timer.
Point 1. would be timer.start() --> will start a timer with the parameters
Point 3. can be addressed with the callback that will be called if the timer runs out --> createTimer(5,'display', ()=>{ // your code goes here })
Point 2. can be achieved with --> timer.stop()
Point 4. is needed when the timer needs to be reset without running out timer.reset()
Further on the interval is not in the global scope so you could have multiple timers with different settings and they wouldn't interfere with each other
// function for creating the timer
function createTimer(seconds, cssSelector, callbackOnTimeout) {
// interval the timer is running
let interval;
// the html node where innerText will be set
const display = document.getElementById(cssSelector)
// original seconds passt to createTimer needed for restart
const initSec = seconds
// starting or continuing the interval
function start() {
// setting interval to the active interval
interval = setInterval(() => {
display.innerText = `00:${seconds}`;
--seconds;
if (seconds < 0) {
// calling restart and callback to restart
callbackOnTimeout()
restart()
}
}, 1000);
}
// just stopping but not resetting so calling start will continue the timer
// player takes a break
function stop(){
clearInterval(interval)
}
// opted for a restart and not only a reset since it seemed more appropriate for your problem
function restart(){
clearInterval(interval)
seconds = initSec
start()
}
// returning the object with the functions
return {
start: start,
stop: stop,
restart: restart
}
}
// example for creating a timer
const timer1 = createTimer(5,'display',()=>{
console.log(`you where to slow ohhh...`)
})
// calling the timer
timer1.start()
I'm writing some Javascript that interacts with library code that I don't own, and can't (reasonably) change. It creates Javascript timeouts used for showing the next question in a series of time-limited questions. This isn't real code because it is obfuscated beyond all hope. Here's what the library is doing:
....
// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = setTimeout( showNextQuestion(questions[i+1]), t );
I want to put a progress bar onscreen that fills towards questionTime * 1000 by interrogating the timer created by setTimeout. The only problem is, there seems to be no way to do this. Is there a getTimeout function that I'm missing? The only information on Javascript timeouts that I can find is related only to creation via setTimeout( function, time) and deletion via clearTimeout( id ).
I'm looking for a function that returns either the time remaining before a timeout fires, or the time elapsed after a timeout has been called. My progress bar code looks like this:
var timeleft = getTimeout( test.currentTimeout ); // I don't know how to do this
var $bar = $('.control .bar');
while ( timeleft > 1 ) {
$bar.width(timeleft / test.defaultQuestionTime * 1000);
}
tl;dr: How do I find the time remaining before a javascript setTimeout()?
Here's the solution I'm using now. I went through the library section that's in charge of tests, and unscrambled the code (terrible, and against my permissions).
// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = mySetTimeout( showNextQuestion(questions[i+1]), t );
and here's my code:
// wrapper for setTimeout
function mySetTimeout( func, timeout ) {
timeouts[ n = setTimeout( func, timeout ) ] = {
start: new Date().getTime(),
end: new Date().getTime() + timeout
t: timeout
}
return n;
}
This works pretty spot-on in any browser that isn't IE 6. Even the original iPhone, where I expected things to get asynchronous.
Just for the record, there is a way to get the time left in node.js:
var timeout = setTimeout(function() {}, 3600 * 1000);
setInterval(function() {
console.log('Time left: '+getTimeLeft(timeout)+'s');
}, 2000);
function getTimeLeft(timeout) {
return Math.ceil((timeout._idleStart + timeout._idleTimeout - Date.now()) / 1000);
}
Prints:
$ node test.js
Time left: 3599s
Time left: 3597s
Time left: 3595s
Time left: 3593s
This doesn't seem to work in firefox through, but since node.js is javascript, I thought this remark might be helpful for people looking for the node solution.
EDIT: I actually think I made an even better one: https://stackoverflow.com/a/36389263/2378102
I wrote this function and I use it a lot:
function timer(callback, delay) {
var id, started, remaining = delay, running
this.start = function() {
running = true
started = new Date()
id = setTimeout(callback, remaining)
}
this.pause = function() {
running = false
clearTimeout(id)
remaining -= new Date() - started
}
this.getTimeLeft = function() {
if (running) {
this.pause()
this.start()
}
return remaining
}
this.getStateRunning = function() {
return running
}
this.start()
}
Make a timer:
a = new timer(function() {
// What ever
}, 3000)
So if you want the time remaining just do:
a.getTimeLeft()
If you can't modify the library code, you'll need to redefine setTimeout to suit your purposes. Here's an example of what you could do:
(function () {
var nativeSetTimeout = window.setTimeout;
window.bindTimeout = function (listener, interval) {
function setTimeout(code, delay) {
var elapsed = 0,
h;
h = window.setInterval(function () {
elapsed += interval;
if (elapsed < delay) {
listener(delay - elapsed);
} else {
window.clearInterval(h);
}
}, interval);
return nativeSetTimeout(code, delay);
}
window.setTimeout = setTimeout;
setTimeout._native = nativeSetTimeout;
};
}());
window.bindTimeout(function (t) {console.log(t + "ms remaining");}, 100);
window.setTimeout(function () {console.log("All done.");}, 1000);
This is not production code, but it should put you on the right track. Note that you can only bind one listener per timeout. I haven't done extensive testing with this, but it works in Firebug.
A more robust solution would use the same technique of wrapping setTimeout, but instead use a map from the returned timeoutId to listeners to handle multiple listeners per timeout. You might also consider wrapping clearTimeout so you can detach your listener if the timeout is cleared.
Server side Node.js specific
None of the above really worked for me, and after inspecting the timeout object it looked like everything was relative to when the process started. The following worked for me:
myTimer = setTimeout(function a(){console.log('Timer executed')},15000);
function getTimeLeft(timeout){
console.log(Math.ceil((timeout._idleStart + timeout._idleTimeout)/1000 - process.uptime()));
}
setInterval(getTimeLeft,1000,myTimer);
Output:
14
...
3
2
1
Timer executed
-0
-1
...
node -v
v9.11.1
Edited output for brevity, but this basic function gives a approximate time until execution or since execution. As others mention, none of this will be exact due to the way node processes, but if I want to suppress a request that was run less than 1 minute ago, and I stored the timer, I don't see why this wouldn't work as a quick check. Could be interesting to juggle objects with refreshtimer in 10.2+.
Javascript's event stacks don't operate how you would think.
When a timeout event is created, it is added to the event queue, but other events may take priority while that event is being fired, delay the execution time and postponing runtime.
Example: You create a timeout with a delay of 10 seconds to alert something to the screen. It will be added to the event stack and will be executed after all current events are fired (causing some delay). Then, when the timeout is processed, the browser still continues to capture other events add them to the stack, which causes further delays in the processing. If the user clicks, or does a lot of ctrl+typing, their events take priority over the current stack. Your 10 seconds can turn into 15 seconds, or longer.
That being said, there are many ways to fake how much time has passed. One way is to execute a setInterval right after you add the setTimeout to the stack.
Example: Perform a settimeout with a 10 second delay (store that delay in a global). Then perform a setInterval that runs every second to subtract 1 from the delay and output the delay remaining. Because of how the event stack can influence actual time (described above), this still won't be accurate, but does give a count.
In short, there is no real way to get the remaining time. There are only ways to try and convey an estimate to the user.
A quicker, easier way:
tmo = 1000;
start = performance.now();
setTimeout(function(){
foo();
},tmo);
You can get the time remaining with:
timeLeft = tmo - (performance.now() - start);
I stopped by here looking for this answer, but was overthinking my problem. If you are here because you just need to keep track of time while you're setTimeout is in progress, here's another way to do it:
var focusTime = parseInt(msg.time) * 1000
setTimeout(function() {
alert('Nice Job Heres 5 Schrute bucks')
clearInterval(timerInterval)
}, focusTime)
var timerInterval = setInterval(function(){
focusTime -= 1000
initTimer(focusTime / 1000)
}, 1000);
You can modify setTimeout to store each timeout's end time in a map and create a function called getTimeout to get the time left for a timeout with a certain id.
This was super's solution, but I modified it to use slightly less memory
let getTimeout = (() => { // IIFE
let _setTimeout = setTimeout, // Reference to the original setTimeout
map = {}; // Map of all timeouts with their end times
setTimeout = (callback, delay) => { // Modify setTimeout
let id = _setTimeout(callback, delay); // Run the original, and store the id
map[id] = Date.now() + delay; // Store the end time
return id; // Return the id
};
return (id) => { // The actual getTimeout function
// If there was no timeout with that id, return NaN, otherwise, return the time left clamped to 0
return map[id] ? Math.max(map[id] - Date.now(), 0) : NaN;
}
})();
Usage:
// go home in 4 seconds
let redirectTimeout = setTimeout(() => {
window.location.href = "/index.html";
}, 4000);
// display the time left until the redirect
setInterval(() => {
document.querySelector("#countdown").innerHTML = `Time left until redirect ${getTimeout(redirectTimeout)}`;
},1);
Here's a minified version of this getTimeout IIFE:
let getTimeout=(()=>{let t=setTimeout,e={};return setTimeout=((a,o)=>{let u=t(a,o);return e[u]=Date.now()+o,u}),t=>e[t]?Math.max(e[t]-Date.now(),0):NaN})();
I hope this is as useful to you as it was for me! :)
No, but you can have your own setTimeout/setInterval for animation in your function.
Say your question looks like this:
function myQuestion() {
// animate the progress bar for 1 sec
animate( "progressbar", 1000 );
// do the question stuff
// ...
}
And your animation will be handled by these 2 functions:
function interpolate( start, end, pos ) {
return start + ( pos * (end - start) );
}
function animate( dom, interval, delay ) {
interval = interval || 1000;
delay = delay || 10;
var start = Number(new Date());
if ( typeof dom === "string" ) {
dom = document.getElementById( dom );
}
function step() {
var now = Number(new Date()),
elapsed = now - start,
pos = elapsed / interval,
value = ~~interpolate( 0, 500, pos ); // 0-500px (progress bar)
dom.style.width = value + "px";
if ( elapsed < interval )
setTimeout( step, delay );
}
setTimeout( step, delay );
}
If anyone's looking back on this. I've come out with a timeout and interval manager that can get you the time left in a timeout or interval as well as do some other stuff. I'll be adding to it to make it more nifty and more accurate, but it seems to work fairly well as is (although I have some more ideas to make it even more accurate):
https://github.com/vhmth/Tock
Question has already been answered but I will add my bit. It just occured to me.
Use setTimeout in recursion as follows:
var count = -1;
function beginTimer()
{
console.log("Counting 20 seconds");
count++;
if(count <20)
{
console.log(20-count+"seconds left");
setTimeout(beginTimer,2000);
}
else
{
endTimer();
}
}
function endTimer()
{
console.log("Time is finished");
}
I guess the code is self explanatory
Check this one:
class Timer {
constructor(fun,delay) {
this.timer=setTimeout(fun, delay)
this.stamp=new Date()
}
get(){return ((this.timer._idleTimeout - (new Date-this.stamp))/1000) }
clear(){return (this.stamp=null, clearTimeout(this.timer))}
}
Make a timer:
let smtg = new Timer(()=>{do()}, 3000})
Get remain:
smth.get()
Clear timeout
smth.clear()
(function(){
window.activeCountdowns = [];
window.setCountdown = function (code, delay, callback, interval) {
var timeout = delay;
var timeoutId = setTimeout(function(){
clearCountdown(timeoutId);
return code();
}, delay);
window.activeCountdowns.push(timeoutId);
setTimeout(function countdown(){
var key = window.activeCountdowns.indexOf(timeoutId);
if (key < 0) return;
timeout -= interval;
setTimeout(countdown, interval);
return callback(timeout);
}, interval);
return timeoutId;
};
window.clearCountdown = function (timeoutId) {
clearTimeout(timeoutId);
var key = window.activeCountdowns.indexOf(timeoutId);
if (key < 0) return;
window.activeCountdowns.splice(key, 1);
};
})();
//example
var t = setCountdown(function () {
console.log('done');
}, 15000, function (i) {
console.log(i / 1000);
}, 1000);
For anyone in need of a hook, check this out - should be pretty self explanatory.
Note that elapsed is an internal state variable that if passed outside of the hook will be incorrect!
import { useEffect, useRef, useState } from 'react';
const useTimeout = (callback, duration, renderDuration = 5) => {
const ref = useRef<any>(null);
const [timeInfo, setTimeInfo] = useState<{
start: number;
elapsed: number;
percentComplete: number;
}>({
start: null,
elapsed: 0,
percentComplete: 0
});
useEffect(() => {
return () => {
if (ref.current) {
clearTimeout(ref.current);
ref.current = null;
}
};
}, []);
useEffect(() => {
setTimeout(() => {
if (ref.current == null) return;
setTimeInfo((prev) => {
const elapsed = Date.now() - prev.start + prev.elapsed;
if (ref.current == null) return prev;
return {
start: prev.start,
elapsed: prev.elapsed,
percentComplete: (elapsed / duration) * 100
};
});
}, renderDuration);
}, [timeInfo]);
return {
percentComplete: timeInfo.percentComplete,
isTimerRunning: ref.current != null,
startTimeout: () => {
if (ref.current != null) return;
setTimeInfo((prev) => ({ ...prev, start: Date.now() }));
ref.current = setTimeout(callback, duration - timeInfo.elapsed);
},
stopTimeout: () => {
if (ref.current) {
clearTimeout(ref.current);
ref.current = null;
}
setTimeInfo((prev) => {
const elapsed = Date.now() - prev.start + prev.elapsed;
return {
start: prev.start,
elapsed: elapsed,
percentComplete: (elapsed / duration) * 100
};
});
},
resetTimeout: () => {
if (ref.current) {
ref.current = null;
clearTimeout(ref.current);
}
setTimeInfo({ start: null, elapsed: 0, percentComplete: 0 });
},
restartTimeout: () => {
if (ref.current) {
ref.current = null;
clearTimeout(ref.current);
}
setTimeInfo({ start: Date.now(), elapsed: 0, percentComplete: 0 });
ref.current = setTimeout(callback, duration);
}
};
};
export default useTimeout;