DEV Community

Cover image for Creating a countdown timer RxJS vs Vanilla JS
Jinto Jose
Jinto Jose

Posted on • Updated on

Creating a countdown timer RxJS vs Vanilla JS

Let's see our requirements.

  1. Create a Countdown timer
  2. start button which will start the timer from the current stage
  3. pause button which will pause the timer, so that, we can resume the timer on clicking start again
  4. stop button which will stop the timer, and reset the timer number.

Timer

Let's see how we can do with Vanilla JS.

<h3 id="result"></h3>
<button id="startBtn">Start</button>
<button id="pauseBtn">Pause</button>
<button id="stopBtn">Stop</button>
Enter fullscreen mode Exit fullscreen mode

Let's first select the elements and add click listeners

const startBtn = document.querySelector('#startBtn');
const stopBtn = document.querySelector('#stopBtn');
const pauseBtn = document.querySelector('#pauseBtn');
const result = document.querySelector('#result');

startBtn.addEventListener('click', () => {
  //start the interval
});

stopBtn.addEventListener('click', () => {
  //stop the interval and reset value in HTML
});

pauseBtn.addEventListener('click', () => {
  // pause the interval
});

Enter fullscreen mode Exit fullscreen mode

We will have to create few variables.

  1. to store the current value
  2. to store the initial value
  3. to store the interval ( Since we want to do an action continuously on a specific interval, we will use setInterval )
let interval;
const initialValue = 10;
let currentValue = initialValue;
Enter fullscreen mode Exit fullscreen mode

We will also set the current value to the HTML

result.innerHTML = `${currentValue}`;
Enter fullscreen mode Exit fullscreen mode

Now, we will create the function to start the timer and call this function on click of start button

const startInterval = () => {
  clearInterval(interval);

  interval = setInterval(() => {
    currentValue -= 1;
    if (currentValue <= 0) {
      currentValue = initialValue;
      clearInterval(interval);
    }
    result.innerHTML = `${currentValue}`;
  }, 1000);
};

startBtn.addEventListener('click', () => {
  startInterval();
});
Enter fullscreen mode Exit fullscreen mode

On click of stop button, we will clear the interval, and also reset the value.

stopBtn.addEventListener('click', () => {
  currentValue = initialValue;
  clearInterval(interval);
  result.innerHTML = `${currentValue}`;
});

Enter fullscreen mode Exit fullscreen mode

On click of Pause button, we are just clearing the interval, and not resetting the value.

pauseBtn.addEventListener('click', () => {
  clearInterval(interval);
});
Enter fullscreen mode Exit fullscreen mode

Here is the whole code.

Now, lets try the same with RxJS

First, same selectors again

const startBtn = document.querySelector('#startBtn');
const stopBtn = document.querySelector('#stopBtn');
const pauseBtn = document.querySelector('#pauseBtn');
const counterDisplayHeader = document.querySelector('h3');
Enter fullscreen mode Exit fullscreen mode

Now, lets create event streams for the button clicks

const startClick$ = fromEvent(startBtn, 'click');
const stopClick$ = fromEvent(stopBtn, 'click');
const pauseBtn$ = fromEvent(pauseBtn, 'click');
Enter fullscreen mode Exit fullscreen mode

Let's define a starting value, so that, countdown can start from any number defined.

const startValue = 10;
Enter fullscreen mode Exit fullscreen mode

Now, the RxJS magic

merge(startClick$.pipe(mapTo(true)), pauseBtn$.pipe(mapTo(false)))
  .pipe(
    switchMap(shouldStart => (shouldStart ? interval(1000) : EMPTY)),
    mapTo(-1),
    scan((acc: number, curr: number) => acc + curr, startValue),
    takeWhile(val => val >= 0),
    startWith(startValue),
    takeUntil(stopClick$),
    repeat()
  )
  .subscribe(val => {
    counterDisplayHeader.innerHTML = val.toString();
  });

Enter fullscreen mode Exit fullscreen mode

Let's try to breakdown

First, we will just try only the start. On click of start, we want to start an interval.

startClick$
  .pipe(
    switchMapTo(interval(1000)),

Enter fullscreen mode Exit fullscreen mode

and, we want to decrement the value by 1 and start the value from the starting value. so, we will use two operators here

startClick$
  .pipe(
    switchMapTo(interval(1000)),
    mapTo(-1),
    scan((acc: number, curr: number) => acc + curr, startValue)
Enter fullscreen mode Exit fullscreen mode

Now, we need to have option to stop the timer. We want to stop the timer on two scenarios.

  1. When the value reaches 0
  2. When user press stop button
startClick$
  .pipe(
    switchMapTo(interval(1000)),
    mapTo(-1),
    scan((acc: number, curr: number) => acc + curr, startValue),
    takeWhile(val => val >= 0),
    takeUntil(stopClick$)
Enter fullscreen mode Exit fullscreen mode

We want to start with a value of startValue

startClick$
  .pipe(
    switchMapTo(interval(1000)),
    mapTo(-1),
    scan((acc: number, curr: number) => acc + curr, startValue),
    takeWhile(val => val >= 0),
    startWith(startValue),
    takeUntil(stopClick$)
  )
Enter fullscreen mode Exit fullscreen mode

Now, in case of pause button click, we just want to emit an empty observable.

pauseBtn$
    .pipe(
      switchMapTo(EMPTY))
    )
Enter fullscreen mode Exit fullscreen mode

Finally, we want to combine both start and pause button clicks. We are not really interested in event details. instead, we just want to decide between interval or EMPTY observable based on click. So, we will just map the button clicks to true or false. if start button is clicked, map the value to true and if pause button is clicked, we map the value to false, so that, we can check on switchMap.

merge(startClick$.pipe(mapTo(true)), pauseBtn$.pipe(mapTo(false)))
  .pipe(
    switchMap(shouldStart => (shouldStart ? interval(1000) : EMPTY))
Enter fullscreen mode Exit fullscreen mode

And, we want to start again once the timer is stopped. For that, we are using repeat() operator

And, you can see and play around with the whole code here,

So, with RxJS, I didn't have to create any extra external variables, intervals etc. Also, no need to add separate logic for start, stop, pause. Whole logic is added in a single chain of commands.

Isn't it neat? What do you think of this? Is there any better way to do this?

Let me know in comments.

Top comments (0)