Angular Icon Get 67% off the Angular Master Bundle!

See the bundle then add to cart and your discount is applied.

0 days
00 hours
00 mins
00 secs
Angular

Angular Loading Spinners with Router Events

In this post you’ll learn how to create a loading spinner that will show and hide based on your application’s loading state. To achieve this, we’ll be using the Angular Router and hooking into some of the events provided.

A loading spinner is typically best displayed in the root component, which is usually our AppComponent.

First off let’s start with an empty component, import and inject our Router. This will then enable us to subscribe to the Router events:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      <h1>My App</h1>
      <router-outlet></router-outlet>
    </div>
  `,
})
export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {}
}

Inside our OnInit lifecycle hook we’ll setup a subscription to the events property on the Router:

@Component({...})
export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.subscribe(console.log);
  }
}

In our developers tool console we’ll then see something like (with various metadata inside each class object):

▶ NavigationStart {...}
▶ RouteConfigLoadStart {...}
▶ RouteConfigLoadEnd {...}
▶ RoutesRecognized {...}
▶ GuardsCheckStart {...}
▶ ActivationStart {...}
▶ GuardsCheckEnd {...}
▶ ResolveStart {...}
▶ ResolveEnd {...}
▶ ActivationEnd {...}
▶ NavigationEnd {...}

That’s a lot of events! And this is a good thing.

To find the events that are relevant to our goal of creating a loading spinner, we’re going to follow a simple process of filtering out the events we do need and then toggling a loading state accordingly.

First, let’s define a new loading$ property which contains the $ suffix to denote an Observable type. We’ll then create a new Observable using of():

import { Observable, of } from 'rxjs';

@Component({...})
export class AppComponent implements OnInit {
  loading$: Observable<boolean> = of(false);

  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.subscribe(console.log);
  }
}

We can then subscribe in the template to our loading$ Observable using the Async Pipe:

<div *ngIf="loading$ | async">Loading...</div>

Our next step is to pipe() and filter() on the events we’re interested in:

import { Observable, of } from 'rxjs';
import { filter } from 'rxjs/operators';

@Component({...})
export class AppComponent implements OnInit {
  loading$: Observable<boolean> = of(false);

  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.pipe(
      filter(
        (e) =>
          e instanceof NavigationStart ||
          e instanceof NavigationEnd ||
          e instanceof NavigationCancel ||
          e instanceof NavigationError
      )
    )
    // ONLY runs on:
    // NavigationStart, NavigationEnd, NavigationCancel, NavigationError
    .subscribe(console.log);
  }
}

As Observables are lazy by design, any further logic we write will only run when these navigation events are found, instead of running on any type of router event.

So now the last step is simply to set loading$ to true when we are inside a NavigationStart and as soon as NavigationEnd, NavigationCancel or NavigationError fire we’ll set it back to false.

To do this, we can simply map() a new value back and check if the NavigationStart event is the one we’ve received. If it is, we’ll return true and false if not:

import { Observable, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';

@Component({...})
export class AppComponent implements OnInit {
  loading$: Observable<boolean> = of(false);

  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.pipe(
      filter(
        (e) =>
          e instanceof NavigationStart ||
          e instanceof NavigationEnd ||
          e instanceof NavigationCancel ||
          e instanceof NavigationError
      ),
      map((e) => e instanceof NavigationStart)
    )
    // true or false
    .subscribe(console.log);
  }
}

As we are using map() we’ll return a new value to our .subscribe() each time it fires. To finish things off we’ll need to point our Observable to the this.loading$ and remove the manual subscription:

import { Observable, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';

@Component({...})
export class AppComponent implements OnInit {
  loading$: Observable<boolean> = of(false);

  constructor(private router: Router) {}

  ngOnInit() {
    this.loading$ = this.router.events.pipe(
      filter(
        (e) =>
          e instanceof NavigationStart ||
          e instanceof NavigationEnd ||
          e instanceof NavigationCancel ||
          e instanceof NavigationError
      ),
      map((e) => e instanceof NavigationStart)
    );
  }
}

Nice and clean! These events will fire when navigating between your application components. For best results use a Route Guard to load the data for a particular page, as this will force the NavigationEnd event to only fire when the data has resolved.

If you are serious about your Angular skills, your next step is to take a look at my Angular courses where you’ll learn Angular, TypeScript, RxJS and state management principles from beginning to expert level.

Happy spinning!

Free eBooks:

JavaScript Array Methods eBook Cover

Ready to go beyond ForEach? Get confident with advanced methods - Reduce, Find, Filter, Every, Some and Map.

NestJS Build a RESTful CRUD API eBook Cover

Build your first NestJS app. With the CLI you'll learn the basics of real-world NestJS development.