Loading spinners are an essential requirement for any Angular app to indicate the application is busy. This is often the case when an HTTP request is in progress. While it is possible to manually show and hide a loading spinner, we can avoid code duplication with a custom HttpInterceptor.

In this post, you’ll create a custom HttpInterceptor that shows and hides a loading spinner on every HTTP request.

Angular Material loading spinner

Import Angular Material

The first step is to import Angular Material library references to your project. You can check out my guide or the official docs.

If you want to use another library for the loading spinner, you do not need Angular Material.

Create a SpinnerService

We need to create a service to keep track of loading spinner visibility.

Create a new service called SpinnerService:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root'})
export class SpinnerService {
  visibility: BehaviorSubject<boolean>;

  constructor() {
    this.visibility = new BehaviorSubject(false);
  }

  show() {
    this.visibility.next(true);
  }

  hide() {
    this.visibility.next(false);
  }
}Code language: TypeScript (typescript)

The purpose of this service is to allow any component in the application to read and update the visibility status.

It also broadcasts the status change to any class that subscribes to the visibility property.

When the visibility changes, all listeners will be notified of this change. Through data binding in Angular, this helps us show and hide the loading spinner.

Create a custom HttpInterceptor

HttpInterceptor is an interface provided in @angular/common/http package. It offers an intercept() method, which allows us to intercept and handle HttpRequest and HttpResponse.

Let’s create a custom interceptor to update spinner visibility using the SpinnerService.

Create a new file called http-interceptor.ts:

import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpResponse } from '@angular/common/http';
import { HttpRequest } from '@angular/common/http';
import { HttpHandler } from '@angular/common/http';
import { HttpEvent } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { SpinnerService } from './spinner.service';

@Injectable()
export class CustomHttpInterceptor implements HttpInterceptor {

     constructor(private spinnerService: SpinnerService) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

        this.spinnerService.show();

        return next.handle(req)
             .pipe(tap((event: HttpEvent<any>) => {
                    if (event instanceof HttpResponse) {
                        this.spinnerService.hide();
                    }
                }, (error) => {
                    this.spinnerService.hide();
                }));
    }
}Code language: TypeScript (typescript)

The interceptor shows the spinner before handling the request. When the response is received from the request, we simply hide the spinner.

If the request fails for any reason, we hide the spinner using the error callback.

Tell Angular to use the interceptor

We now need to tell Angular to use our custom interceptor for all HTTP requests.

To do this, we use provide our interceptor against HTTP_INTERCEPTORS inside the AppModule.

@NgModule({
  declarations: [ AppComponent ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule
  ],
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: CustomHttpInterceptor,
    multi: true
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }Code language: TypeScript (typescript)

Notice I specify true for multi property – this is to prevent this interceptor from overriding other interceptors.

If you do not have another interceptor, feel free to remove that line.

Display loading spinner

The ideal place for putting the spinner component is usually the root component.

For the purpose of this tutorial, I am placing the loading spinner in the app.component.html file:

<mat-spinner [diameter]="50" *ngIf="spinnerService.visibility | async">
</mat-spinner>

<button mat-raised-button color="primary" (click)="getEmployees()">
Get Employees
</button>Code language: HTML, XML (xml)

A couple of things are going on here..

I first adjust the size of the spinner with diameter property.

And I am using *ngIf directive to decide whether it is rendered or not. This depends on the visibility value of the SpinnerService. The use of async pipe resolves the value immediately in the template.

Of course, we need to ensure the spinner service is injected into the component:

import { Component } from '@angular/core';
import { SpinnerService } from './spinner.service';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  constructor(public spinnerService: SpinnerService,
    private httpClient: HttpClient) { }

  getEmployees() {
    this.httpClient.get<any>('http://dummy.restapiexample.com/api/v1/employees')
      .subscribe(success => {
        console.log('Done');
      }, error => {
        console.error('Error');
      });
  }
}Code language: TypeScript (typescript)

I placed a button to make a simple HTTP request so that we can see the spinner during the request.

Angular Material loading spinner

You can also use the progress bar component instead of a loading spinner in the HTML template:

<mat-progress-bar color="accent" mode="indeterminate" *ngIf="spinnerService.visibility | async"></mat-progress-bar>Code language: HTML, XML (xml)

Handling concurrent calls

It is not uncommon for applications to make multiple, concurrent API calls to load data.

Our implementation so far may lead to unwanted scenarios such as spinner flashing or not hiding at all. We can avoid this behaviour by showing the spinner with the first request and hide it when all requests complete.

Let’s improve our SpinnerService to keep track of multiple calls:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class SpinnerService {
  count = 0;
  visibility: BehaviorSubject<boolean>;

  constructor() {
    this.visibility = new BehaviorSubject(false);
  }

  show() {
    this.visibility.next(true);
    this.count++;
  }

  hide() {
    this.count--;
    if (this.count === 0) {
      this.visibility.next(false);
    }
  }
}
Code language: TypeScript (typescript)

As you can see in the code snippet above, we’re now keeping a count of calls that require a spinner.

We increment the count when showing the spinner and decrement it when hiding it.

Knowing the number of ‘open’ requests helps us determine when to hide the spinner. As you guessed it, we only hide the spinner when all requests complete.

Summary

In this post, we looked at how to show and hide a spinner in Angular using a custom interceptor. We used Angular Material spinner and progress bar to give visual feedback to the user.

Check out the complete project on GitHub.

Umut Esen

Software Engineer specialising in full-stack web application development.

Leave a Reply

This Post Has 6 Comments

  1. Zsolt

    What about multiple, concurrent API calls?

    1. Umut Esen

      Hey Zsolt, you’d need to keep track of number of concurrent calls and hide the spinner when all calls complete. I’ve updated the post to include a section on this.
      Thanks!

  2. Jefin George Jose

    How exactly is the variable “showSpinner” communicating with the visibility variable in spinner service?

  3. Ajay Saini

    Thank you. It is really a nice article.

  4. Ale

    But using this interceptor, the other request that I had at the app does not work anymore. How can I to get this interceptor to work only to show/hide the spinner?

    1. Umut Esen

      The order of interceptors matter, so ensure you add the spinner interceptor last with multi flag set to true.