Search code examples
javascriptangularrxjsngrx-store

Show navigation loader spinner with delay in angular 2+


I'm new to angular 2+ and RxJS, trying to get used to RxJS.

I show load spinner on route transitions, but only if it takes more then certain amount time, lats say 160ms
I have a load spinner as a separate component, with subscription to ngrx store, so I show/hide load spinner based on value in the sore (showSpinner)

In my app root component, I subscribe to router change events, and dispatch actions (SHOW_SPINNER/HIDE_SPINNER)
So the question is, Is there a simpler way to achieve it?

Here are parts of my code

....

export const navigationStreamForSpinnerStatuses = {
  NAVIGATION_STARTED: 'NAVIGATION_STARTED',
  NAVIGATION_IN_PROGRESS: 'NAVIGATION_IN_PROGRESS',
  NAVIGATION_ENDED: 'NAVIGATION_ENDED'
};

....

private navigationStartStream;
private navigationStartStreamWithDelay;
private navigationFinishStream;

constructor(private store: Store<IAppState>, private router: Router) {
  this.navigationStartStream = router.events
    .filter(event => {
      return event instanceof NavigationStart;
    })
    .map(() => navigationStreamForSpinnerStatuses.NAVIGATION_STARTED);

  this.navigationStartStreamWithDelay = this.navigationStartStream
    .delay(160)
    .map(() => navigationStreamForSpinnerStatuses.NAVIGATION_IN_PROGRESS);

  this.navigationFinishStream = router.events
    .filter(event => {
      return event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError;
    })
    .map(() => navigationStreamForSpinnerStatuses.NAVIGATION_ENDED);

  this.navigationStartStream
    .merge(this.navigationFinishStream)
    .merge(this.navigationStartStreamWithDelay)
    .pairwise()
    .subscribe([previousStatus, currentStatus] => {
      if (previousStatus !== navigationStreamForSpinnerStatuses.NAVIGATION_ENDED && currentStatus === navigationStreamForSpinnerStatuses.NAVIGATION_IN_PROGRESS) {
        this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.SHOW_SPINNER });
      } else if (previousStatus === navigationStreamForSpinnerStatuses.NAVIGATION_IN_PROGRESS && currentStatus === navigationStreamForSpinnerStatuses.NAVIGATION_ENDED) {
        this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.HIDE_SPINNER });
      }
    });
}

Solution

  • Utilize the takeUntil operator to cancel your spinner timer if it returns before the time. Additionally, create a hot observable using timer to trigger an action after a certain time has passed.

    takeUntil Returns the values from the source observable sequence until the other observable sequence or Promise produces a value.

    https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/takeuntil.md

    timer Returns an observable sequence that produces a value after dueTime has elapsed and then after each period.

    https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/timer.md

    You can simplify your logic here by handling the dispatch directly on each stream.

    this.navigationEnd$ = router.events
      .filter(event => event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError);
    
    this.navigationStart$ = router.events
      .filter(event => event instanceof NavigationStart)
      .subscribe(_ => {
        Observable.timer(160)
          .takeUntil(this.navigationEnd$)
          .subscribe(_ => this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.SHOW_SPINNER });
      });
    
    this.navigationEnd$.subscribe(_ => this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.HIDE_SPINNER });
    

    So what we've done is listen to the start of the navigation and start a timer for 160ms. If the navigation end event happens before the timer, no spinner will show (takeUntil). Otherwise, the store action will be dispatched and the spinner will show. Regardless of whether or not the spinner is showing, after the navigation finishes we dispatch the hide spinner action.