Having a global loading status in your web application is useful, but not easy to get right.

The challenge lies in catching all the HTTP API requests and displaying a loading bar, spinner or textual status whenever there is server communication.

Alongside this blogpost, you can also find a working example at Stackblitz.

Angular 4+ has a new HttpClient which supports HttpInterceptor.

This allows you to insert code that will be run whenever you make a HTTP request. This is great for catching all the requests, but we also need a custom service to share information of whether we we have pending requests or not. This because the interceptor itself should not be shared.

Architectural flow when intercepting an HTTP request to create a loader status
Architectural flow when intercepting an HTTP request to create a loader status

As a minimum example, we should have:

An HttpInterceptor class “LoaderInterceptor” that intercepts all HTTP requests and tells the “LoaderService” whether we have pending requests or not:

// There are a lot of missing pieces here, so bear with me for now
@Injectable() export class LoaderInterceptor implements HttpInterceptor {
   constructor(private loaderService: LoaderService) { }

   // This is the code that will run whenever an HttpRequest is made
   intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent> {
       this.loaderService.isLoading.next(true);
       return next.handle(req).map(
           () => this.loaderService.isLoading.next(false)
       );
   }
}

A “LoaderService” that contains an “isLoading” stream on which the HttpInterceptor can push the status, and which components can subscribe on to get the status:

@Injectable()
export class LoaderService {
   // A BehaviorSubject is an Observable with a default value
   public isLoading = new BehaviorSubject(false);
   constructor() {}
}

A Component that connects the “LoaderService” with the template:

@Component({...})
export class MyComponent {
   constructor(public loaderService: LoaderService) {}
}

An HTML/Angular template which shows the “isLoading” status:

<div *ngIf="loaderService.isLoading | async; else notLoading">Loading!</div>
<ng-template #notLoading>Not Loading!</ng-template>

This seems pretty straightforward, but there are some pitfalls.

First of all, you probably notice that the interceptor code assumes only one HTTP request is made at a time.

If two requests are made, we will turn off the loader as soon as the first response is back.

Let’s handle this by saving all pending requests in an array, and make a function to remove a request when it’s time to do so:

@Injectable()
export class LoaderInterceptor implements HttpInterceptor {
   // We need to store the pending requests
   private requests: HttpRequest[] = [];
   constructor(private loaderService: LoaderService) { }

   removeRequest(req: HttpRequest) {
       const i = this.requests.indexOf(req);
       if ( i >= 0) {
          this.requests.splice(i, 1); // This removes the request from our array
       }
       // Let's tell our service of the updated status
       this.loaderService.isLoading.next(this.requests.length > 0);
   }

   intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent> {
       this.requests.push(req); // Let's store this request
       this.loaderService.isLoading.next(true);
       next.handle(req).map(() => { this.removeRequest(req); });
   }
}

It is important to notice that the response from a HttpRequest is a short-lived Observable that terminates after the response, error or timeout. That means you do not have to unsubscribe from it manually. That’s great!.

Furthermore, if the observable is unsubscribed before the response has returned, the request is canceled and neither of the handlers will be processed.

You may therefore end up with a “hanging” loader bar, which never goes away.

This typically happens if you navigate a bit fast in your application, which causes your component to be destroyed and subscription automatically unsubscribed.

To get around this last issue, we need to create a new Observable to be able to attach teardown logic. Teardown logic is code that is being processed automatically when an Observable is being unsubscribed to.

We then return this new Observable rather than the original Observable. Since they are not connected, we also need to forward events, errors and closed state so that you as a developer will not notice that the interceptor is there at all.

So, let’s have a look at our final LoaderInterceptor Class:

@Injectable()
export class LoaderInterceptor implements HttpInterceptor {
 private requests: HttpRequest[] = [];

 constructor(private loaderService: LoaderService) { }

 removeRequest(req: HttpRequest) {
   const i = this.requests.indexOf(req);
   if (i >= 0) {
     this.requests.splice(i, 1);
   }
   this.loaderService.isLoading.next(this.requests.length > 0);
 }

 intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent> {
   this.requests.push(req);
   this.loaderService.isLoading.next(true);
   // We create a new observable which we return instead of the original
   return Observable.create(observer => {
     // And subscribe to the original observable to ensure the HttpRequest is made
     const subscription = next.handle(req)
       .subscribe(
       event => {
         if (event instanceof HttpResponse) {
           this.removeRequest(req);
           observer.next(event);
         }
       },
       err => { this.removeRequest(req); observer.error(err); },
       () => { this.removeRequest(req); observer.complete(); });
     // return teardown logic in case of cancelled requests
     return () => {
       this.removeRequest(req);
       subscription.unsubscribe();
     };
   });
 }
}

Our Component needs no change. We use the same LoaderService and with the async operator we do not even need to subscribe.

Since the source value we want to use is from a service, it is shared as an Observable (BehaviorSubject inherits from Observable) so that it gets a rendering scope/zone where it is used. If it is just a value, it may not update your GUI as wanted.

The final touch is to provide the services to the app. I usually create a separate module in the “core” directory which I then import into the AppModule:

@NgModule({
   providers: [
     LoaderService,
     { provide: HTTP_INTERCEPTORS,
       useClass: LoaderInterceptor,
       multi: true
     }
   ]
})
export class LoaderModule { }

We have now shown how you can implement a loader status in your Angular application. You could also push the number of pending requests to the LoaderService if your application requires it.

Good luck, and remember to check out the Stackblitz example.

Om Jørn Are Hatlelid

Jørn Are is front end tech-lead in Computas with more than 20 years of experience with the web. He has been coding and delivering projects using Angular for almost 3 years, and AngularJS before that. Testability and code quality stands close to his heart.

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *