DEV Community

Cover image for Loading indication in Angular
Nils Mehlhorn for Angular

Posted on • Originally published at nils-mehlhorn.de

Loading indication in Angular

Originally published at nils-mehlhorn.de on January 30, 2019

Its a common desire: having something rotate or fly around to entertain the user while the moderately performing backend fishes data from god knows where. Though it seems easy to borrow a spinner from CodePen and display it while you make the server roundtrip, there are some common misconceptions and pitfalls that we'll clear up.

Waiting for data

Let's start with a routine task: we want to display a list of users which is fetched asynchronously through a service. An inexperienced, yet technically fine, solution could look as follows:

export class UserComponent implements OnInit  {

  users: User[]
  loading = false

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.loading = true
    this.userService.getAll().pipe(
      finalize(() => this.loading = false)
    ).subscribe(users => this.users = users)
  }
}
Enter fullscreen mode Exit fullscreen mode

An instance variable is used for holding the users and another one for a flag indicating whether the users are still loading or have already arrived. Before subscribing - and thus kicking of the asynchronous call - the loading flag is updated. After the call completes, it's reset through the use of the finalize operator. The callback passed to this operator will be called after the observable call completes - regardless of its result. If this was rather just done inside the subscription callback, the loading flag would only reset after a successful call and not in case of an error. A corresponding view could look like this:

<ul *ngIf="!loading">
  <li *ngFor="let user of users">
    {{ user.name }}
  </li>
</ul>
<loading-indicator *ngIf="loading"></loading-indicator>
Enter fullscreen mode Exit fullscreen mode

Yet, for most calls which provide data to be displayed directly into a view, this setup can be simplified using the AsyncPipe. Our component will be shortened to the following:

export class UserComponent implements OnInit  {

  users$: Observable<User[]>

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.users$ = this.userService.getAll()
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the component directly exposes the stream of users to the view. We'll update the view using the async as syntax to
bind the stream's value to a separate users variable once it emits:

<ul *ngIf="users$ | async as users; else indicator">
  <li *ngFor="let user of users">
    {{ user.name }}
  </li>
</ul>
<ng-template #indicator>
  <loading-indicator></loading-indicator>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

By providing a view template for the else block of the *ngIf we don't have to manage a loading flag explicitly anymore. This approach is more declarative as it connects both view states via an if-else connection instead of having two separate if-blocks. Also, we don't have to manage the stream's subscription ourselves anymore as this is done by the pipe (including un-subscribing when the component is destroyed).

Waiting for actions

The AsyncPipe lets us down when we're dealing with actions like creating a new user upon a button click. You'll have to subscribe inside your component at some point when you cannot pipe the observable back into the view.

First of, while some may disagree, I think it's valid to use the flag-approach this time. Don't follow false prophets condemning the slightest redundancy. Many times it should be about making code easy to understand, test and also delete instead of ending up with the least possible line count. So, it's pretty much fine doing it like this:

<button (click)="create()">Create User</button>
<div *ngIf="loading">
  Creating, please wait <loading-indicator></loading-indicator>
</div>
Enter fullscreen mode Exit fullscreen mode
export class UserComponent  {

  loading: boolean

  constructor(private userService: UserService) {}

  create(name = "John Doe"): void {
    this.loading = true
    this.userService.create(new User(name)).pipe(
      finalize(() => this.loading = false)
    ).subscribe()
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, lets see what we can do if you're dead set against those two lines for switching the loading flag explicitly in every component.

Interceptor approach

I've seen people recommend using an HttpInterceptor to observe whether any calls are being currently processed. Such an interceptor could look along the lines of this:

@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
  constructor(private loadingService: LoadingService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // you could also check for certain http methods here
    this.loadingService.attach(req);
    return next.handle(req).pipe(
      finalize(() => this.loadingService.detach(req)),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

With this implementation a separate LoadingService is notified when a request starts of via attach(req). Once the request finishes - with whatever result - this service is again notified via detach(req). The service could check upon each call whether there are still any open requests and thus manage a global loading flag.

I've also used this approach myself - even back in AngularJS.
And while I'd deem it a decent approach for giving the user generic indication of when the app's loading, you've really
got to consider three things:

1) You lose specificity. As you're not having a loading flag per-request but rather a global one, you just can't know for certain which request is still taking its time. While you could inject the service into any of your components and that way display local indication - the information you're really having is on a global application level. It'd be just semantically wrong to use it as an indicator for a single request.

2) What is the user meant to do with a global loading indication? Do you disable everything as long as anything in your app is still loading? Should the user wait until your global indication is done? What if a request that's unrelated to the user's current task got stuck?

3) You're weirdly coupled. You've gone all the way to hide the HTTP calls behind a service and carefully separate them from view logic just so you can now go behind your own back. I won't completely condemn it, just something to think about.

Know what you want

If you're fine with the trade-offs of the interceptor approach, you could use it for a global indication like a progress bar going from the top left to the top right corner of the application window - illustration below.

Global Loading Indication

Global Loading Indication

A few mainstream apps are doing it this way, it looks kinda fancy and there are already a couple tutorials and decent libraries out there taking full care of such behaviour.

Still, if we want to tell our user what's exactly wasting his time or even disable certain interactions in the meantime (again, illustration below), we'll have to come up with something else.

Contextual Loading Indication

Contextual Loading Indication

It's crucial to know the difference. Otherwise you might indicate that your form action (e.g. bottom left in the illustration) is still loading although another call originating from somewhere else on the page (e.g. far right widget in the illustration) is the actual culprit.

Don't get fooled by simplicity, instead know what you want and then we can figure out how to build it.

Reactive contextual approach

If you want specific, contextual loading indication without explicitly flipping the loading flag, you could do that using RxJS operators. Since RxJS 6 it's possible to define your own operators in form of pure functions. Firstly, we'll have an operator which invokes a callback upon subscription. This can be done using the RxJS method defer:

export function prepare<T>(callback: () => void): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>): Observable<T> => defer(() => {
    callback();
    return source;
  });
}
Enter fullscreen mode Exit fullscreen mode

Now we create another operator accepting a subject as our sink for the loading state. Using our newly created prepare operator, we'll update this subject upon subscription to the actual source stream via indicator.next(true). Similarly, we use the finalize operator to inform it about the loading being completed via indicator.next(false):

export function indicate<T>(indicator: Subject<boolean>): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>): Observable<T> => source.pipe(
    prepare(() => indicator.next(true)),
    finalize(() => indicator.next(false))
  )
}
Enter fullscreen mode Exit fullscreen mode

We can then use the new indicate operator in our component as follows:

export class UserComponent  {
  loading$ = new Subject<boolean>()

  constructor(private userService: UserService) {}

  create(name = "John Doe"): void {
    this.userService.create(new User(name))
    .pipe(indicate(this.loading$))
    .subscribe()
  }
}
Enter fullscreen mode Exit fullscreen mode
<button (click)="create()">Create User</button>
<div *ngIf="loading$ | async">
  Creating, please wait <loading-indicator></loading-indicator>
</div>
Enter fullscreen mode Exit fullscreen mode

I've put the snippets together into a complete example on StackBlitz. Reload to see the first indicator and click 'Create User' to see the second. Also, don't get distracted by the blue indicators from StackBlitz, ours are red.

Top comments (4)

Collapse
 
johncarroll profile image
John Carroll

I ended up writing a friendly response post to this: dev.to/johncarroll/angular-how-to-....

It also includes some changes I've made to the IsLoadingService API, since updating it to support dynamic key values (unfortunately, supporting dynamic keys means that the return of isLoading$() can no longer be cached).

Collapse
 
johncarroll profile image
John Carroll

Incidentally I made an Angular library so solve this very task a while back: IsLoadingService.

In general you can simply hook it up in your root component and forget about it:

@Component({
  selector: 'app-root',
  template: `
    <mat-progress-bar
      *ngIf="(loadingService.isLoading$() | async)"
      mode="indeterminate"
      color="warn"
      style="position: absolute; top: 0; z-index: 100;"
    >
    </mat-progress-bar>

    <router-outlet></router-outlet>
  `,
})
export class AppComponent implements OnInit {
  constructor(public loadingService: IsLoadingService) {}
}

It will handle indicating loading during router navigation. So long as you use route resolvers and route guards, I find the majority of get related requests are covered.

Regarding mutations:

export class UserComponent  {
  constructor(
    private userService: UserService,
    public isLoadingService: IsLoadingService,
  ) {}

  create(name = "John Doe"): void {
    // loading turns off when the request completes
    this.isLoadingService.add(
      this.userService.create(new User(name)).subscribe()
    )
  }
}

Or sometimes I'll have a persistent observable connection, and I only want to indicate loading while it is fetching the first batch of data but then after that hide any loading from the user. In this case you can just pass an observable to IsLoadingService#add() and it will take(1) from the observable and then pass the observable back to the caller. I.e.

@Component({
  template: `
    <div *ngIf="isLoadingService.isLoading$() | async">
      <!-- this will only display during initial loading -->
      Creating, please wait <loading-indicator></loading-indicator>
    </div>

    <div *ngFor='let user of users$ | async'>
      <!-- do stuff -->
    </div>
  `
})
export class UserComponent  {
  users$: Observable<User>;

  constructor(
    private userService: UserService,
    public isLoadingService: IsLoadingService,
    private angularFire: AngularFirestore,
  ) {}

  ngOnInit() {
    // here we let the `async` pipe take care of unsubscribe for us
    this.users$ =
      this.isLoadingService.add(
        this.angularFire.collection(usersCollection).valueChanges(),
      );
  }
}
Collapse
 
n_mehlhorn profile image
Nils Mehlhorn

Interesting, but that would give you global indication again, right? You couldn't realiably use it for the 'Creating, please wait'-part as some other request might also cause the loading.

Another suggestion: it'd probably be good to get rid of the function call in the template loadingService.isLoading$(). Its usually called everytime change detection runs. As you've already got observables there, it shouldn't be that hard - you could probably transfer the method arguments into something that's operator based.

Collapse
 
johncarroll profile image
John Carroll • Edited

Ya I noticed my original comment was getting really long so I cut short the details, but you can call IsLoadingService#add() with an optional key argument which allows you to trigger separate loading indicators (though in practice, I've only ever used this option once or twice).

isLoadingService.add(subscription, {key: 'my-key'})

isLoadingService.isLoading$({key: 'my-key'})

Regarding the function call in the template, I thought about that but, in testing, decided removing it was a solution in search of a problem. The function is merely returning a cached value and, even running every change detection cycle, is very performant. If it is ever shown to be a problem I'll certainly address it, but, at the moment, I'm skeptical that any change would be an improvement.

Edit
I could imagine the function call might be a problem if you were rendering hundreds (maybe dozens) of loading indicators on the screen at once, but I think this scenario would only occur if you were using dynamic key values to, for example, show which rows in a spreadsheet were loading. This scenario is already not supported.