1

I have in my angular app many api calls, where I want to show a loading component, if data is coming from server.

For this I have a loader service like this:

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

@Injectable({ providedIn: 'root' })
export class LoaderService {
  isLoading = new Subject<boolean>();

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

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

I have an httpInterceptor too, where I use the show and hide methods like this:

intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    this.loaderService.show();

    return new Observable((observer) => {
      next.handle(request).subscribe(
        (res) => {
          observer.next(res);
        },
        (err: HttpErrorResponse) => {
          this.loaderService.hide();
          if (err.error.message) {
            this.customNotificationService.showNotification(
              err.error.message,
              3000,
              'error'
            );
          } else {
            this.customNotificationService.showNotification(
              'Ismeretlen eredetű hiba. Lépj kapcsolatba a rendszer üzemeltetőjével.',
              3000,
              'error'
            );
          }
        },
        () => {
          this.loaderService.hide();
        }
      );
    });
  }

In the component, where I want to use the loading component, I have this in the template:

<loading-overlay
  [visible]="loadingOverlayVisible | async"
  loadingText=""
></loading-overlay>

And in the ts:

loadingOverlayVisible: Subject<boolean> = this.loaderService.isLoading;

This works except one case: ngOnInit. When the component loads, I load the initial data like this:

ngOnInit() {
    this.gridData = { data: [], total: 0 };
    this.userService.getUsers().subscribe((users) => {
      users.forEach((user) => {
        // we get this as string from the backend
        user.lastLoginDateDisplay = new Date(user.lastLoginDateDisplay!);
      });
      this.gridData = { data: users, total: users.length };
    });
  }

The data gets loaded, but without any loading indicator. If I change the logic and use the standard subscribe/unsubscribe way, it works. But it would be more cleaner/smarter with the async pipe.

So, the question is: how to do that?

derstauner
  • 1,478
  • 2
  • 23
  • 44

1 Answers1

2

LoaderService.isLoading should be a BehaviorSubject instead. I'm not sure, but I think ngInit finishes before the template is first evaluated. If I'm right, then LoaderService.isLoading has already emitted true, and when your async pipe subscribes, it's too late.

JSmart523
  • 2,069
  • 1
  • 7
  • 17
  • I tried it and it worked. I'm already aware of BehaviorSubject, but could you maybe explain, why it works with BehaviorSubject and not wit Subject? Maybe because of this: Current value is either latest value emitted by source observable using next() method or initial/default value. – derstauner Mar 03 '22 at 21:17
  • 1
    See this answer here for the difference between a `Subject` and a `BehaviorSubject`. https://stackoverflow.com/a/43351340/821918 – spots Mar 03 '22 at 21:24
  • 1
    I'm glad! Please remember to mark this as the correct answer. @spots provided a great link. A Subject is a "hot" observable. Unlike `from([1,2,3])` that will emit 1, 2, and 3 per subscription, subscribing to a hot observable is like beginning to watch a pipe, or playground slide. Subscribing won't tell you what happened before you subscribed. BehaviorSubject, though, *also* also tells each subscriber the last thing emitted before that subscriber had subscribed! Without that, you're waiting for a train that's already left the station and no one's around to tell you. – JSmart523 Mar 03 '22 at 21:57
  • Of course I mark it as an answer. I'm happy to have a very good answer/explanation. I like this analogy. – derstauner Mar 04 '22 at 06:31