Creating a countdown timer RxJS vs Vanilla JS
Tags: rxjs, javascript, observable, operators
Published: 2021-09-13 15:35:06


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xl5y5dfyanr93kb8n335.png)


Let's see how we can do with Vanilla JS. 
```html
<h3 id="result"></h3>
<button id="startBtn">Start</button>
<button id="pauseBtn">Pause</button>
<button id="stopBtn">Stop</button>
```

Let's first select the elements and add click listeners
```js
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
});

```
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 )

```js
let interval;
const initialValue = 10;
let currentValue = initialValue;
```

We will also set the current value to the HTML
```js
result.innerHTML = `${currentValue}`;
```

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

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

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

startBtn.addEventListener('click', () => {
  startInterval();
});
```

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

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

```

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

```js
pauseBtn.addEventListener('click', () => {
  clearInterval(interval);
});
```

Here is the whole code. 

{% stackblitz js-ugvkmx file=index.js %}

Now, lets try the same with <b>RxJS</b>

First, same selectors again

```js
const startBtn = document.querySelector('#startBtn');
const stopBtn = document.querySelector('#stopBtn');
const pauseBtn = document.querySelector('#pauseBtn');
const counterDisplayHeader = document.querySelector('h3');
```
Now, lets create event streams for the button clicks

```js
const startClick$ = fromEvent(startBtn, 'click');
const stopClick$ = fromEvent(stopBtn, 'click');
const pauseBtn$ = fromEvent(pauseBtn, 'click');
```

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

```js
const startValue = 10;
```

Now, the RxJS magic
```js
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();
  });

```
<img src="https://media.giphy.com/media/2sddCIZRYfiPlNeLZn/giphy.gif">


Let's try to breakdown

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

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

```

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

```js
startClick$
  .pipe(
    switchMapTo(interval(1000)),
    mapTo(-1),
    scan((acc: number, curr: number) => acc + curr, startValue)
```
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

```js
startClick$
  .pipe(
    switchMapTo(interval(1000)),
    mapTo(-1),
    scan((acc: number, curr: number) => acc + curr, startValue),
    takeWhile(val => val >= 0),
    takeUntil(stopClick$)
```
We want to start with a value of startValue
```js
startClick$
  .pipe(
    switchMapTo(interval(1000)),
    mapTo(-1),
    scan((acc: number, curr: number) => acc + curr, startValue),
    takeWhile(val => val >= 0),
    startWith(startValue),
    takeUntil(stopClick$)
  )
```
Now, in case of pause button click, we just want to emit an empty observable.

```js
pauseBtn$
    .pipe(
      switchMapTo(EMPTY))
    )
```
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.

```js
merge(startClick$.pipe(mapTo(true)), pauseBtn$.pipe(mapTo(false)))
  .pipe(
    switchMap(shouldStart => (shouldStart ? interval(1000) : EMPTY))
```

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, 

{% stackblitz rxjs-xluwzo file=index.ts %}

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.