Search code examples
rxjsobservable

How to add a stop and start feature for an RxJS timer?


I added a start, stop, pause button. Start will start a count down timer which will start from a value, keep decrementing until value reaches 0. We can pause the timer on clicking the pause button. On click of Stop also timer observable completes.

  • However, once the timer is completed ( either when value reaches 0 or when clicked on stop button ), I am not able to start properly. I tried adding repeatWhen operator. It starts on clicking twice. Not at the first time.

  • Also, at stop, value is not resetting back to the initial value.

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

Stackblitz Code link is available here


Solution

  • This is a pretty complicated usecase. There are two issues I think:

    • You have two subscriptions to startClick$ and the order of subscriptions matters in this case. When the chain completes repeatWhen is waiting for startClick$ to emit. However, when you click the button the emission is first propagated into the first subscription inside merge(...) and does nothing because the chain has already completed. Only after that it resubscribes thanks to repeatWhen but you have to press the button again to trigger the switchMap() operator.

    • When you use repeatWhen() it'll resubscribe every time the inner Observable emits so you want it to emit on startClick$ but only once. At the same time you don't want it to complete so you need to use something like this:

      repeatWhen(notifier$ => notifier$.pipe(
        switchMap(() => startClick$.pipe(take(1))),
      )),
      

    So to avoid all that I think you can just complete the chain using takeUntil(stopClick$) and then immediatelly resubscribe with repeat() to start over.

    merge(
      startClick$.pipe(mapTo(true)),
      pauseBtn$.pipe(mapTo(false))
    )
      .pipe(
        switchMap(val => (val ? interval(10) : 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();
      });
    

    Your updated demo: https://stackblitz.com/edit/rxjs-tum4xq?file=index.ts