1

I'm doing a web application in Angular 8 and I want to show a loading spinner while doing a HTTP request.

The loading spinner is not showing with my implementation and I could not find the reason.

Service

@Injectable()
export class UserService {

  // Caching
  private bsResource: BehaviorSubject<string[]> = new BehaviorSubject([]);
  private readonly resources$: Observable<string[]> = this.bsResource.asObservable();

  constructor(
    private http: HttpClient
  ) {
  }

  // Calling this method the loading spinner is not showing.
  getResources(): Observable<string[]> {
    if (this.bsResource.getValue().length === 0) {
      this.fetchResourceList().toPromise().then(res => this.bsResource.next(res));
    }

    return this.resources$;
  }

  // The loading spinner appears if I use this method directly and make this method public.
  private fetchResourceList(): Observable<string[]> {
    return this.http.get<string[]>('MY_URL');
  }
}

Component:

@Component({
  templateUrl: './create-new.component.html',
  styleUrls: ['./create-new.component.scss']
})
export class CreateNewComponent {
  resourceList$: Observable<string[]> = this.service.getResources();

  constructor(
    private service: UserService
  ) { }
}

Template

<div class="form-group">
  <label for="resourceSelect">* Resources</label>
  <div *ngIf="resourceList$ | async as resourceList; else loading">
     <ng-container *ngIf="resourceList.length; else noResults" >
        <div *ngFor="let r of resourceList; index as i">
          <!-- Show the result using checkboxes -->
        </div>
     </ng-container>
  </div>
</div>

<ng-template #loading>
  <br/>
  <div class="spinner-border spinner-border-sm text-muted" role="status">
    <span class="sr-only">Loading...</span>
  </div>
</ng-template>

<ng-template #noResults>
  <div class="text-muted">No results.</div>
</ng-template>

I don't understand why by using this.service.getResources() the loading spinner is not showing and if I use this.service.fetchResourceList() the loading spinner appears correctly.

My goal is to show the loading spinner correctly using the example that I have provided and keeping the method that I'm calling in my component.

9
  • Your code looks so complicated for such a simple call... Did you clean your code before posting it, or is it the full code ? Commented Apr 24, 2020 at 13:50
  • 1
    @Random Hi, I have deleted a lot of things that is not important for my question. I have cleaned it before posting it, that is the exact code that I use today. It seems large because I'm implementing a local cache to save the information in memory to avoid unnecessary HTTP requests in the future. At least that is how I see it. Commented Apr 24, 2020 at 13:54
  • Do you see any errors in console? Can you also confirm if the http call is made successfully? Commented Apr 24, 2020 at 13:55
  • @KarthickManoharan Hi, there is no errors in console and I can confirm that the HTTP call is made correctly. The information/result from the request is showing, but the loading spinner doesn't. Commented Apr 24, 2020 at 13:58
  • Thanks for confirming. Can you also make sure by delaying the response time by throttling the speed? Because if the API is fast to respond then the spinner might not be displayed Commented Apr 24, 2020 at 14:02

1 Answer 1

1

I spent time to reproduce it in a sandbox, but finally found that the problem comes from the initialization of your BehaviorSubject. The first value of bsResource is []. So in your *ngIf, when the pipe async subscribes to it, it receives the value [], which is truthy, so the *ngIf is instantly true, and never triggers the else (loading) block.

All you have to do is to initialize your BehaviorSubject with a null value (and then fix the code using it, to be null-safe):

private bsResource: BehaviorSubject<string[]> = new BehaviorSubject(null);

And when you use it:

const cachedValue = this.bsResource.getValue();
if (!cachedValue || cachedValue.length === 0) {
  this.fetchResourceList().subscribe(res => this.bsResource.next(res));
}

I'm saying it again, you should not use Promises, use .subscribe, which works the same as toPromise().then. You should NEVER use Promise. .toPromise is only usefull when you are upgrading Angular, where most of the code uses Promises, and don't want to upgrade the whole code at once.

Sign up to request clarification or add additional context in comments.

9 Comments

You said that I need to initialize BehaviorSubject with null but, then you add it initialized with [], I'm confused now. Another thing is that I think there is a logic issue inside of your if statement, since the default value is null then, is impossible to enter that statement. If that will be the first time that the application is run, then, it will never enter to that statement because it's obviously null and obviously is not length === 0. Also, if we are subscribing we should unsubscribe after saving the result, I'm wrong? Is not good to keep the subscription open.
@RRGT19 indeed, I copy-pasted a bit too quickly. Fixed the 2 lines of code :) Also, yes you have to unsubscribe (with a takeUnit, or an attribute saving the subscription)
If we are subscribing we should unsubscribe after saving the result, I'm wrong? Is not good to keep the subscription open. I can unsubscribe after saving the result? I don't know what's the correct thing to do in this case.
Yes you can. Naturally, I would prefere completing the BehaviorSubject, or use take(1) or something like this, but it is not that simple, have to check if it would work... The easiest/safest way is indeed to unsubscribe inside the subscribe. But beware, you will first receive a null, and then the actual value. So you may add a filter pipe before the subscribe, or add an if inside the subscribe.
Yeah, .toObservable().pipe( filter(r => r !== null), take(1) ) would do the trick, I guess
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.