Search code examples
typescriptrxjstogglerxjs6togglebutton

stop API calls using a toggle start/stop button Rxjs


I've been learning Rxjs, and have created a little slideshow that refreshed the images every 5 sec once the start button is clicked. How can I get the API to stop/pause once the button is clicked again?

import './style.css';
import { tap, switchMap } from 'rxjs/operators';
import { fromEvent, Observable, timer } from 'rxjs';

function updateImages(
  links: string[]
  // link1: string,
  // link2: string,
  // link3: string
): void {
  document.getElementById('slideshow').childNodes.forEach((node: ChildNode) => {
    if (node.nodeType == Node.ELEMENT_NODE) {
      if (links.length) {
        let element: HTMLElement = node as HTMLElement;

        element.classList.add('loading');
        element.style.backgroundImage = "url('" + links.shift() + "')";
        element.classList.remove('loading');
      }
    }
  });
}

//api returns message w jpeg url & status
const apiUrl: string = 'https://dog.ceo/api/breeds/image/random';

const btn = document.getElementById('btn'); // get the button element

const btnEvents$ = fromEvent(btn, 'click');

const sub = btnEvents$.subscribe((result) => {
  startPolling('dogs').subscribe((dogs) => {
    updateImages(dogs.map(({ message }) => message));
  });
  btn.innerHTML = 'Stop';
  console.log(btn.classList);
});

function requestData(url: string): Observable<Array<{ message: string }>> {
  return Observable.create((observer) => {
    Promise.all([
      fetch(url).then((response) => response.json()),
      fetch(url).then((response) => response.json()),
      fetch(url).then((response) => response.json()),
      fetch(url).then((response) => response.json()),
    ])
      .then((dogs) => {
        console.log(dogs);
        observer.next(dogs);
        observer.complete();
      })
      .catch((err) => observer.error(err));
  }).pipe(tap((data) => console.log('dogrequest', data)));
}
function startPolling(
  category: string,
  interval: number = 5000
): Observable<Array<{ message: string }>> {
  const url = category === 'dogs' ? apiUrl : null;
  console.log(url);
  return timer(0, interval).pipe(switchMap((_) => requestData(url)));
}

at the moment it keeps fetching data and I can't get it to stop.


Solution

  • If I understand right what you are trying to achieve, you have a button which behaves like a switch, in the sense that when it is clicked it starts the slideshow (switch on) and when it is clicked again it stops the slideshow (switch off).

    If my understanding is right, this is a good case for the use of the switchMap operator.

    Let's see how it could be done. The comments explain the code

    // fist of all we define a variable holding the state of the switch
    // there are more 'rxJs idiomatic' ways to manage the state, but for the moment
    // lets stay with a variable holding the state
    let on = false;
    
    // this is your code that defines the btnEvents$ Observable which is the 
    // starting point of our stream
    const btn = document.getElementById('btn'); // get the button element
    const btnEvents$ = fromEvent(btn, 'click');
    
    // this is the core stream that implements our solution
    // it starts with the stream of 'click' events
    btnEvents$
      .pipe(
        // the tap operator allows us to implement side effects
        // in this particular case every time a click is notified we need to 
        // change the state (switch on/off) and set the label of the button
        tap(() => {
          on = !on;
          btn.innerHTML = on ? 'Stop' : 'Start';
        }),
        // here is where the switch happens
        // every time a click is notified by upstream, the switchMap operator
        // unsubscribes the previous Observable and subscribes to a new one
        switchMap(() => {
          // this means that when a new click event is notified by upstream
          // we check the state
          // if the state is "on" we return the stream that fetches the data
          // otherwise we return an Observable that notifies just an empty array
          return on ? requestData("dogsUrl") : of([]);
        }),
        // finally last side effect, i.e. we update the images based on the 
        // arrays of dogs returned
        // consider that if the state is "off" than the "dogs" array is empty
        // and therefore no update is performed
        tap(dogs => updateImages(dogs.map(({ message }) => message));)
      )
      // now that we have built the Observable as per our requirements, 
      // we just have to subscribe to it to trigger the entire execution
      .subscribe(console.log);
    

    The implementation is not complete, for instance it lacks the error handling logic, but I hope it is enough to clarify the idea