Creating a timer in JavaScript sounds quite simple. And on the face of it, it really is. With the built-in setTimeout
function on the window
object available in all browsers, it's quite easy to do the programatic equivalent of saying: "After 10 seconds, do this thing". It looks like this:
setTimeout(() => {
doSomething();
}, 10_000);
This isn't a typo, by the way, 10_000
is a legit way to write the number 10,000 in JavaScript. It's just a bit cleaner and easier to read. Funnily enough, I learnt this from Ruby, where the same syntax works. Neat.
Aside from the dodgy way that JavaScript handles numbers and counting, this works as intended. After 10 seconds (10,000 milliseconds) the doSomething
function is called.
Custom Timer - Our First Iteration
But what if I want to pause it for some reason? Maybe because the user clicked a 'pause' button, most obviously. Or what about when the element where the timer appears scrolls out of the viewport? This is the use case I faced recently. A more complex solution is necessary. The timer won't pause automatically unless we make it happen.
It would also be useful to have independent instances of timers so that we can have multiple running together. I'd rather not create duplicated code by calling setTimeout
several times in that case, so I'll create a reusable Timer
class that will contain the logic for me. Whenever I need a new timer, I will create a new instance of that class. Let's create that class and add a way to pause and resume each timer individually.
I opted to use TypeScript here to add that little bit of type safety that we often appreciate. Here's how it looks:
class Timer {
// This is called when we create the timer instance with
// new Timer(...)
constructor(callback: () => void, duration: number) {
this.remaining = duration;
this.callback = callback;
this.isPaused = false;
this.resume();
}
// We'll keep track of the timer and the function
// called after it's completed with private instance variables
private timerId?: number;
private start?: number;
private remaining: number;
private callback: () => void;
public isPaused: boolean;
// Pausing the timer clears the timeout set on window
pause = () => {
window.clearTimeout(this.timerId);
// We keep track of how long is remaining on the timer
// in the private variable `this.remaining`
this.remaining -= Number(new Date()) - Number(this.start);
// We also flip `this.isPaused` to know the timer is paused
this.isPaused = true;
};
resume = () => {
// Set `this.start` to know how long is remaining if the timer
// is paused
this.start = Number(new Date());
// Clear away any leftover timer with the same ID to make sure
window.clearTimeout(this.timerId);
// Assign `this.timerId` to the number returned from `setTimeout`
// This is handled by the browser, we don't need to generate it
this.timerId = window.setTimeout(this.callback, this.remaining);
// We also flip `this.isPaused` back to know the timer continues
this.isPaused = false;
};
}
Let's take a look at how this works with a quick example below. Use the buttons to start, pause, and resume the timer. Once the timer runs out, we see a message indicating that it is finished.
For reference, the React component (written in TypeScript) for these buttons looks like this:
const TimerExample = ({ showRemaining }: Props) => {
const timer = useRef<Timer | null>(null);
const [alert, showAlert] = useState(false);
const startTimer = () => {
showAlert(false);
let interval: number;
timer.current = new Timer(() => {
showAlert(true);
window.clearInterval(interval);
}, 3_000);
};
const pauseTimer = () => {
timer.current?.pause();
};
const resumeTimer = () => {
timer.current?.resume();
};
return (
<div>
<button onClick={startTimer}>Start 3 second timer</button>
<br />
<button onClick={pauseTimer}>Pause 3 second timer</button>
<br />
<button onClick={resumeTimer}>Resume 3 second timer</button>
{alert && <p>TIMER HAS ENDED!</p>}
</div>
);
};
This satisfies the requirement that something should happen when the timer runs out, and also that we can manually pause and resume the timer. Our production implementation might not control these actions with a click, but they could be with mouse over, mouse leave, or indeed scrolling an element into or out of the viewport, as discussed before. The simple component above can be easily extended and modified to suit the individual use case and this Timer class will remain reusable for many of those cases.
Second Iteration - Query Remaining Time
Another small 'feature' I wanted to add here was something that bugged me a little when I first started working on this, especially with longer timers - I was dealing regularly with 45 seconds - and that is knowing approximately how long is left on the timer at any given point. You probably also had the same feeling if you played around with the live example above. You click on that 'start' button and you have no idea whether the timer started or not. the only feedback you get there is when the 3 seconds go by and you see the final 'ended' message. This is not ideal for cases where it would be useful to know how long is still to go.
I would like to add some additional functionality to my Timer class that allows me to query the timer instance and see, at any point in time, how long remains on the timer.
Let's add that to our class. It's quite straight-forward when we use the starting timestamp and the current timestamp to calculate how long is left before it runs out:
class Timer {
constructor(callback: () => void = () => {}, duration: number = 0) {
this.remaining = duration;
this.callback = callback;
this.isPaused = false;
this.resume();
}
private timerId?: number;
private start?: number;
private remaining: number;
private callback: () => void;
public isPaused: boolean;
pause = () => {
window.clearTimeout(this.timerId);
this.remaining -= Number(new Date()) - Number(this.start);
this.isPaused = true;
};
resume = () => {
this.start = Number(new Date());
window.clearTimeout(this.timerId);
this.timerId = window.setTimeout(this.callback, this.remaining);
this.isPaused = false;
};
// We've added this public method to query how long the timer has left
getRemaining = () => {
// We know exactly how long is remaining if the timer is paused
// because we're keeping hold of that in this.remaining anyway.
if (this.isPaused) return this.remaining;
// If the timer is running, we need to calculate it on the fly
// using the starting timestamp, the duration (this.remaining)
// and the current timestamp from new Date
return this.start! + this.remaining - Number(new Date());
// Note that this.start will always be defined once we reach here
// So asserting its presence with ! is not risking a runtime error
};
}
The implementation is nice and clean and allows us to find how long the timer has left regardless of whether it's paused or running. Let's see that in action quickly, showing the remaining time rounded to one decimal place for brevity.
The code for this example looks like this:
const TimerExample = ({ showRemaining }: Props) => {
const timer = useRef<Timer | null>(null);
const [remaining, setRemaining] = useState(0);
const [alert, showAlert] = useState(false);
const startTimer = () => {
showAlert(false);
let interval: number;
interval = window.setInterval(() => {
if (!timer.current) return;
const remainingTime = Math.floor(timer.current.getRemaining() / 100) / 10;
setRemaining(Math.max(remainingTime, 0));
}, 20);
timer.current = new Timer(() => {
showAlert(true);
window.clearInterval(interval);
}, 3_000);
};
const pauseTimer = () => {
timer.current?.pause();
};
const resumeTimer = () => {
timer.current?.resume();
};
return (
<div>
<button onClick={startTimer}>Start 3 second timer</button>
<br />
<button onClick={pauseTimer}>Pause 3 second timer</button>
<br />
<button onClick={resumeTimer}>Resume 3 second timer</button>
{showRemaining && <p>Time remaining: {remaining}</p>}
{alert && <p>TIMER HAS ENDED!</p>}
</div>
);
};
There's a little bit more complexity here, with the setTimeout
now just being used to periodically query the timer instance to find out how long is remaining, and also the division that we're doing to convert the remaining time from milliseconds into seconds.
But overall, it's still quite simple. It's unlikely a production application would need this level of detail from the timer.
Wrap Up
That just about does it for today. The timer class has a pretty simple interface, a simple enough implementation and can be reused and even extended where necessary.
One final note from me: this post was almost accompanied by a YouTube video of me actually coding the Timer class live. If you're a developer starting on your journey and would like to see me live-coding this and other JavaScript/TypeScript or even Ruby features and projects that I work on, I'd love to hear from you. Reach out via my contact form, leave me your name, email and a quick message to let me know that this would be useful to you. I don't want to make content that nobody needs, so your feedback would be super valuable to me here. Here's the link to get in touch with me.