How to automatically display loading spinner using Http Interceptor in Ionic 5 ?

In this tutorial, i will show you how to user angular http interceptor to show a loading spinner alert to the user each time a request is made, without having to start or stop manually the spinner.

The source code repository

Each mobile application needs to warn users when a request is made to a server, to let them know that they should wait a little bit. To manage it, most common use case is to display an alert or a loading spinner before the request, and then stopping the alert once the response has been received. And usually we need to manage this behaviour manually, by starting or stopping the alert or spinner from code.

 Angular http interceptor does is allow you to intercept an HttpRequest before it’s sent to the server and after the server responds. So it look the perfect place to automatically start or stop a loading spinner or an alert.

Let's create our Ionic 5 application with command:

ionic start spinner blank --capacitor --project-id=spinner --package-id=com.idevotion.spinner

Then let's add a service with methods to display or stop alerts and to get data from an api.

ionic g service ApiService

Edit the api-service.service.ts file to add the following code

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { LoadingController, AlertController, Platform } from '@ionic/angular';
import { Observable } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class ApiServiceService {

  isShowingLoader = false;
  loader: any;

  constructor(public http: HttpClient,
    public loadingController: LoadingController,
    public alertCtrl: AlertController,
    public platform: Platform) {
  }

  getJsonData() {
    let urlToCall = "https://jsonplaceholder.typicode.com/comments/"
    return Observable.create(observer => {
      this.http.get(urlToCall)
        .subscribe(res => {
          observer.next(res);
          observer.complete();
        }, error => {
          observer.next(null);
          observer.complete();
        });
    });
  }

  async showLoader() {
    if (!this.isShowingLoader) {
      this.isShowingLoader = true
      this.loader = await this.loadingController.create({
        message: 'Please wait',
        duration: 4000
      });
      return await this.loader.present();
    }
  }

  async stopLoader() {
    if (this.loader) {
      this.loader.dismiss()
      this.loader = null
      this.isShowingLoader = false
    }
  }
}

The API Service contains a getJsonData() which will call an API and return json data from it.

And two methods showLoader() and stopLoader() to display a Loadingcontroller showing a waiting message. Nothing really complicated here.

Now let's edit our home.page.html file

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Loading Spinner
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Loading Spinner</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container">
    <strong>Ready to test?</strong>
    <ion-button shape="round" (click)="clickButton()">Get JSON Data</ion-button>
  </div>
</ion-content>

And in our home.page.ts file we create a method clickButton() to :

  1. show the alert
  2. call the API
  3. stop the alert

So let's write our home.page.ts

import { ApiServiceService } from './../api-service.service';
import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  constructor(public apiService:ApiServiceService) {
  }

  clickButton(){
    this.apiService.showLoader().then(()=>{
      this.apiService.getJsonData().subscribe((data)=>{
        console.log(data)
        this.apiService.stopLoader()
      })
    })
  }
}

You can now run ionic serve and have a look to the result. When clicking on the button, an alert message should be displayed, then the alert disappeared and data received from the API should be displayed in the console of your browser.

Ok this is the usual method. Now let's use Angular Http Interceptor to automatically display the alert when a request is initiated and stop the alert once the response is received.

Using angular http interceptor with Ionic 5

We will add the interceptor in our app.module.ts file after the imports and just before the @NgModule

@Injectable()
export class AngularInterceptor implements HttpInterceptor {

  constructor(){
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let defaultTimeout = 10000;
      
      return next.handle(req).pipe(timeout(defaultTimeout),
        retryWhen(err=>{
          let retries = 1;
          return err.pipe(
            delay(500),
            map(error=>{
              if (retries++ ===3){
                throw error
              }
              return error;
            })
          )
        }),catchError(err=>{
          console.log(err)
          return EMPTY
        }), finalize(()=>{
          
        })
      )
    };    
}

The method intercept will be called at the start of every http request which is perfect. This is not the study of your tutorial, but i have added some pipes to the interceptor to add a default timeout of 10 secondes for a request, and if the request failed it will try again 3 times with 500ms between each request. Then after 3 times it will failed.

We will add an observer to our API service that we will use to know when a request starts and when it stops.

 loadingObserver: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

Then we will declare a map that will contain request made

loadingRequestMap: Map<string, boolean> = new Map<string, boolean>();

Now, we will use these two variables in a new method setLoading

 setLoading(loading: boolean, url: string): void {
    if (!url) {
      throw new Error('The request URL must be provided');
    }
    if (loading === true) {
      this.loadingRequestMap.set(url, loading);
      this.loadingObserver.next(true);
    }else if (loading === false && this.loadingRequestMap.has(url)) {
      this.loadingRequestMap.delete(url);
    }
    if (this.loadingRequestMap.size === 0) {
      this.loadingObserver.next(false);
    }
  }

When loading we set the url in our map and we notify our observer that a request has started. And when not loading, we can remove the url and still notify our observers.

With that in place we can use it in our intercept method. Here is the new code

@Injectable()
export class AngularInterceptor implements HttpInterceptor {
  constructor(private _loading: ApiServiceService){

  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let defaultTimeout = 10000;
      this._loading.setLoading(true, req.url);
   
      return next.handle(req).pipe(timeout(defaultTimeout),
        retryWhen(err=>{
          let retries = 1;
          return err.pipe(
            delay(1000),
            map(error=>{
              if (retries++ ===3){
                this._loading.setLoading(false, req.url);
                throw error
              }
              return error;
            })
          )
        }),catchError(err=>{
          console.log(err)
            this._loading.setLoading(false, req.url);
          return EMPTY
        }), finalize(()=>{
            this._loading.setLoading(false, req.url);
        })
      )
    };
}

Ok. Now we just need to subscribe to the loadingObserver variable and show or stop the alert to the user. And because we want this behaviour to be global to our application, we will add the code in our app.component.ts file

export class AppComponent implements OnInit{
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    private apiService:ApiServiceService
  ) {
    this.initializeApp();
  }
  ngOnInit() {
    this.listenToLoading();
  }

  listenToLoading(): void {
    this.apiService.loadingObserver
      .pipe(delay(0)) // This prevents a ExpressionChangedAfterItHasBeenCheckedError for subsequent requests
      .subscribe((loading) => {
        if (loading){
          this.apiService.showLoader()
        }
        else{
          this.apiService.stopLoader()
        }
      });
  }

On the ngOnInit() method we start listening to our variable and then based on loading value we can hide or show an alert to the user.

Finally we can modify our clickButton() method in our home.page.ts file and remove our manual call to start and stop loader.

  clickButton(){
      this.apiService.getJsonData().subscribe((data)=>{
        console.log(data)
      })
  }

If you test again, you still should be able to see the alert message !

Now each time a request will be made in our Ionic application, an alert message will be displayed automatically !

But if for any reasons you don't want to display this alert message when calling some specific urls, you can do it by improving the intercept method and check if the called url should display an alert or not. Something like this:

 if (req.url.indexOf("checkAPI")<0){
      this._loading.setLoading(true, req.url);
    }

It is up to you !

Display a loading spinner with http interceptor with Ionic 5

Now let's modify the code to display a loading spinner instead. Let's modify our home.page.html file

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Loading Spinner
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Loading Spinner</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container">
    <strong>Ready to test?</strong>
    <ion-spinner name="bubbles" *ngIf="loading"></ion-spinner>
    <ion-button shape="round" (click)="clickButton()">Get JSON Data</ion-button>
  </div>
</ion-content>

If the variable loading is true we will display a ion-spinner

Let's change the code in home.page.ts. Now we will subscribe to our observer variable and set the loading variable with the value of our observer

import { ApiServiceService } from './../api-service.service';
import { Component } from '@angular/core';
import { delay } from 'rxjs/operators';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  loading=false;
  constructor(public apiService:ApiServiceService) {

    this.apiService.loadingObserver
    .pipe(delay(0)) // This prevents a ExpressionChangedAfterItHasBeenCheckedError for subsequent requests
    .subscribe((loading) => {
      this.loading = loading
    });
  }

  clickButton(){
      this.apiService.getJsonData().subscribe((data)=>{
        console.log(data)
      })
  }
}

And we can comment the listenToLoading() method in our app.component.ts because we don't want to display the alert message.

  ngOnInit() {
   // this.listenToLoading();
  }

  listenToLoading(): void {
    this.apiService.loadingObserver
      .pipe(delay(0)) // This prevents a ExpressionChangedAfterItHasBeenCheckedError for subsequent requests
      .subscribe((loading) => {
        if (loading){
          this.apiService.showLoader()
        }
        else{
          this.apiService.stopLoader()
        }
      });
  }

Now you can test again and you will be able to see the bubble spinner showing up.

Voilà. Enjoy and don't hesitate to improve the code by yourself !

Christophe Surbier