How to add loading spinner in Angular with RxJS

11 min read


In this article, we'll learn how to add a simple loading spinner in Angular using RxJS and then use an HttpInterceptor to automatically show a loader on all network calls in our Angular app.

Video tutorial

Why a loading spinner

But first, why do we need a loading spinner in Angular? Well, in any web app there are times when you need to tell the user that some process is going on. Usually it is network calls, but could be other background work as well. Since you can need it anywhere on the app, we need to have a global way to do it.

In Angular, services provided at the root are the best for this purpose - because they're instantiated when the app loads and retain their state till the app is destroyed.

Setting up the project

Let's setup our project with the following command.

ng new angular-loading-service

We'll also be adding the material components in a bit.

Creating a loading service with RxJS

But first, let's start with creating the service. We'll use the following command on the Angular CLI.

ng generate service loading

With our new service, let's declare a BehaviorSubject to contain our loading state.

Subject vs BehaviorSubject in RxJS

A Subject in RxJS is an observable which can have more than one subscribers.

A BehaviorSubject is a special type of Subject, which saves the current value as well and emits it to all its subscribers. In this way, it's perfect to store some piece of state in our apps.

If you want to learn more about RxJS and the basics, check out my article on creating a simple weather app with RxJS operators!

Our state here will be either showing the loading spinner or hiding it, so we'll use a boolean and give an initial value of false.

Then, we'll expose a loading observable so that our parent component can access this state. This is a protection against using the Behavior Subject directly because we want the state to only change from the service methods.

@Injectable({
  providedIn: "root",
})
export class LoadingService {
  private _loading = new BehaviorSubject<boolean>(false);
  public readonly loading$ = this._loading.asObservable();

  constructor() {}

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

  hide() {
    this._loading.next(false);
  }
}

Lastly, we just added two functions, one for hiding and one for showing the loading spinner. We use the next function to set the "current" value of the loader.

And that's it for the service! Now let's quickly test this out.

Adding the loading spinner to our Angular app

We'll first add the angular material library by the following command.

ng add @angular/material

Then, let's add the necessary imports in our app.module.

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatToolbarModule,
    HttpClientModule,
    MatProgressSpinnerModule,
  ],
  bootstrap: [AppComponent],
})

Now in our app component we'll create a toolbar and add the progress spinner component and set its mode to indeterminate.

<mat-toolbar color="primary">
  Simple Loading Service with RxJS and HttpInterceptor
</mat-toolbar>
<mat-progress-spinner [mode]="'indeterminate'"></mat-progress-spinner>

And also, we'll add a bit of styling for it to show in the middle of the screen when visible.

mat-progress-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
}

Let's test this out now.

Great, we can see a loading spinner at the correct position in the center.

Now let's link this up with our loading service. We'll declare loading$ observable in our component and add our loading service to the constructor. Then, we'll just equal our variable to the loading observable in our service.

export class AppComponent {
  loading$ = this.loader.loading$;
  constructor(public loader: LoadingService) {}
}

In the template, we'll add an *ngIf directive with the async pipe, so it shows only when the value is true.

Also, we'll add simple buttons to show and hide so we can test this out.

<button mat-raised-button (click)="loader.show()">Show Loader</button>

<button mat-raised-button (click)="loader.hide()">Hide Loader</button>

<mat-progress-spinner
  [mode]="'indeterminate'"
  *ngIf="loading$ | async"
></mat-progress-spinner>

Now, if we test it out, we'll be able to make the loader appear with the "Show Loader" button and make it disappear with the "Hide Loader" button.

Great! This works well but there's a slight problem...

Repetitive calls to show and hide the loading spinner

If you have lots of components with API calls in each of them, it becomes a bit repetitive to show and hide the loader before and after calling the API. Since the vast majority of apps need a loader only when making network calls, we can automate that process a bit.

For that, Angular provides a nifty way called HttpInterceptor.

Http Interceptors basically intercept all network calls using the HttpClient in Angular

We can use them for a variety of purposes such as adding an authorization token etc. Here, we can also use it to show and hide the loading spinner automatically whenever an API call is made. This will save us a lot of time and make our code in our components simpler!

So let's generate an Interceptor using the following command.

ng generate interceptor network

If you look at the interceptor now, we're given an intercept function, which is called whenever an API call is made from the HttpClient. Let's include the loading service in the constructor and then show the loader when the intercept function is called.

@Injectable()
export class NetworkInterceptor implements HttpInterceptor {
  constructor(private loader: LoadingService) {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    this.loader.show();
    return next.handle(request).pipe(
      finalize(() => {
        this.loader.hide();
      })
    );
  }
}

The end of the API call happens after next.handle, so we've added the finalize operator in a pipe after this. The finalize operator in RxJS covers both success and failure cases, so whether the API succeeds or fails, this code should be called. We're just going to hide the loader here.

Registering the interceptor

The last thing we need to do is to tell Angular about the interceptor. We do this by specifying the interceptor in the HTTP INTERCEPTORS token in the providers for our app in app.module.

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: NetworkInterceptor,
      multi: true,
    },
 ],

Great, let's test this out now by making a simple function with a Http call to the Github users API. We'll only log the output in the console. And we'll use a button to call this function. If the interceptor is working fine, this should automatically show the loader when network call is in progress.

fetchUser() {
    this.http
      .get('https://api.github.com/users/thisiszoaib')
      .subscribe((res) => {
        console.log(res);
      });
 }

Let's see if this works.

Nice! So we can see the loader appear and disappear and we also have the output in our console.

So now we have a structure for our apps where we can show a loader automatically when a network call is made and also manually whenever we need it in our components.

Updated: Some improvements to our setup

The example I showed above will be adequate for a lot of web apps. However, when I posted this on twitter, I got a lot of very useful feedback on how this can be made better. It was mostly about how this would not work correctly while multiple network calls happen at the same time.

https://www.twitter.com/zoaibdev/status/1354092228243763200

I also got a request by a viewer on my YouTube channel to show how to add a blur to the background while the loading is in progress.

So let us cover two improvements to our original solution as below.

  • Add a background blur for our content while the loading is in progress
  • Make our spinner better able to handle multiple, concurrent network calls

Adding a blurred background while loading

To disable the background while the loader is in progress, we'll introduce an overlay. Let's create a div in our app and give it a class name of overlay. We'll add some styling in a bit, but before that let's add a parent ng-container and move both the overlay and the spinner inside of it.

Then we'll simply shift the *ngIf condition to the ng container.

<ng-container *ngIf="loading$ | async">
  <div class="overlay"></div>
  <mat-progress-spinner [mode]="'indeterminate'"></mat-progress-spinner>
</ng-container>

The benefit of using an ng-container here is that this won't be rendered in the final HTML, so we won't have any unnecessary divs in our HTML.

Let's add the styling for the overlay. To cover the whole screen, the overlay will have position absolute and top, left, bottom and right properties as zero. We'll make the z-index as 2 and update the spinner z-index to 3 so that it appears on top of the overlay.

mat-progress-spinner {
    ...
    z-index: 3;
}

.overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    backdrop-filter: blur(2px);
}

Also, for the blur effect we've added a backdrop filter on the overlay with a blur of 2px. This will cover everything and will make the content effectively disabled when the loader is shown. Testing it out now, will give you something like below.

Great, that was short and sweet!

Whether you want to keep it or not is your own personal preference. It is good for the overall UX for your app, but make sure all error conditions are handled in this case, because otherwise the user would be effectively locked out of your app if the loader is not stopped.

Handling concurrent network calls

For this second improvement, we first need to see what the problem is with the current Angular loading spinner.

So the current setup works well when we have one network request happening at one time. But what if we have multiple API requests happening in parallel, as can happen in complex web apps today?

Let us test this out with our loader here and see if it works correctly.

We're going to add a new button to fetch multiple sets of data at the same time. And in its handler we'll be making two sets of API calls. One would be the same as before, getting my user details from github. The second one would be two calls one after the other. One would be my user details and the next will be using a concatMap and fetching all the users list from github.

fetchMultipleData() {
    this.http
      .get('https://api.github.com/users/thisiszoaib')
      .subscribe((res) => {
        console.log(res);
      });

      this.http.get('https://api.github.com/users/thisiszoaib').pipe(
        concatMap(() => this.http.get('https://api.github.com/users'))
      )
      .subscribe((res) => {
        console.log(res);
      });

  }

So the two set of calls should take longer than the single one and our loader should only stop when both have ended. We'll log outputs of both in the console to test.

And we'll also open our Dev Tools to check when the data returns. The API calls would be a bit too fast for us to observe, so let's go in our network tab and throttle the speed to Slow 3G.

Ok, time to test this!

So when we click on Fetch Multiple Data, we get the loader as before. But then the loader disappears when the first response comes back. Then, we get our second response, but the loader has already stopped till then.

This is called a race condition in the software world, which means when multiple operations are happening simultaneously the outcome is different depending on the order of the operations. In this case, whichever API returns before, will stop the loader and give an impression to the user that nothing is loading anymore.

Adding a requests counter

One way to deal with this is by adding a counter of the requests currently in process and only stopping the loader when all the network requests have completed.

So let's do that now and then we'll test again!

So we'll go in our network interceptor and create two variables. One would be totalRequests and other would be completedRequests. Both will be initially zero.

Next, when we show our loader, we'll increment our totalRequests because we have a new request in progress now. And whenever any request completes/returns, we'll increment our completedRequests variable. Then we'll check whether the completedRequests are equal to the totalRequests - if they are, then we can safely say that all requests have completed and we can hide the loader.

@Injectable()
export class NetworkInterceptor implements HttpInterceptor {
  totalRequests = 0;
  completedRequests = 0;

  constructor(private loader: LoadingService) {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    this.loader.show();
    this.totalRequests++;

    return next.handle(request).pipe(
      finalize(() => {
        this.completedRequests++;

        console.log(this.completedRequests, this.totalRequests);

        if (this.completedRequests === this.totalRequests) {
          this.loader.hide();
          this.completedRequests = 0;
          this.totalRequests = 0;
        }
      })
    );
  }
}

Great, let's test this out now!

Now our Angular loading spinner will keep on loading till the last of the API call resolving our issue with multiple calls!

You can examine this a bit more closely by adding some logging to the interceptor which shows the total and completed requests, as I've done above.

Conclusion

No piece of software is ever perfect. And that's exactly why we've improved upon our original loading spinner in Angular and made it more robust to handle any number of API calls at the same time, while also adding a simple blur to the background to improve the user experience (UX).

If you have more ideas to make it better, feel free to share on twitter or drop a comment below!

The complete code for this tutorial can be found here.

Thanks for reading! Bye

Support

Liked this post? You can show your love by buying me a coffee - so I can keep bringing you more posts like this. Thanks in advance! 😊

You may also like...