Angular: show loading indicator when obs$ | async is not yet resolved
As a good developer, you always notify the end-user about the current status of an application by showing either loading indicator or error message.

AngularInDepth is moving away from Medium. This article, its updates and more recent articles are hosted on the new platform inDepth.dev
As a good developer, you always notify the end-user about the current status of an application by showing either loading indicator or error message.
Async Pipe
Using async pipe is quite common scenario in many Angular applications.
It is as simple as that:
class AppComponent {
obs$ = of(1).pipe(delay(500));
}
<div>
{{ obs$ | async }}
</div>
Async pipe takes care of subscribing to Observable. It also marks component for check and you don’t worry about unsubscribing.
Built-in *ngIfElse solution
Let’s try to show loading indicator while an underlying async operation is being resolved:
<div *ngIf="obs$ | async as obs; else loading">
{{ obs }}
</div>
<ng-template #loading>Loading...</ng-template>

We leverage as
keyword to assign the result of resolved observable to theobs
local variable. After that, we use thengIfElse
conditional property to display loading indicator if obs
get a falsy value.
At first glance, it looks like a great solution in most cases but let’s discover…
The problems
- Let’s change our observable a bit so that it returns a falsy value:
obs$ = of(0).pipe(delay(500))

2. Let’s simulate an error in our stream:
obs$ = of(1).pipe(
delay(500),
map((x: any) => x()),
);
We see the same screen again:

3. Let’s imagine we use some component that takes loading
property as an Input. How would you pass that property?
<div *ngIf="obs$ | async as obs; else loading">
{{ obs }}
</div>
<ng-template #loading>Loading...</ng-template>
<ng-select [loading]="?????"
All of these problems above introduce a complementary need to some additional code that can be then duplicated again and again across the application.
Custom WithLoadingPipe to the rescue
Not too long ago I posted a solution on twitter where I suggested creating a custom pipe to handle loading behavior.
import { Pipe, PipeTransform } from '@angular/core';
import { isObservable, of } from 'rxjs';
import { map, startWith, catchError } from 'rxjs/operators';
@Pipe({
name: 'withLoading',
})
export class WithLoadingPipe implements PipeTransform {
transform(val) {
return isObservable(val)
? val.pipe(
map((value: any) => ({ loading: false, value })),
startWith({ loading: true }),
catchError(error => of({ loading: false, error }))
)
: val;
}
}
Here’s a simple example of how we can leverage that pipe:
<div *ngIf="obs$ | withLoading | async as obs">
<ng-template [ngIf]="obs.value">Value: {{ obs.value }}</ng-template>
<ng-template [ngIf]="obs.error">Error {{ obs.error }}</ng-template>
<ng-template [ngIf]="obs.loading">Loading...</ng-template>
</div>
Let’s try all the cases we’ve mentioned above with this pipe:

You can also open an ng-run example to play with it. As we can see it fixes all the cases.
Gotcha
Okay, it works well with those simple cases. But what about long-living observables?
Let’s go further and imagine that we’re developing some products page with a search bar:

I used two solutions here: ngIf and the solution with the custom pipe. The full code can be found in Ng-run.com
Our observable is evolved to the following:
searchStream$ = new BehaviorSubject('');
obs$ = this.searchStream$.pipe(
debounceTime(200),
distinctUntilChanged(),
switchMap((query) => this.productsService.getByFilter(query))
);
Please note that in real life cases we should also catch errors in inner observable. Thanks to Wojciech Trawiński for pointing this out.
We emit a new value from searchStream$
once a user types something in input
box.
Let’s see how it behaves now:

As you may have noticed, we can see the loading indicator only on the first load for both options. Not so good (:
Let’s think about how we can fix this behavior without introducing a new component property so that it will show loading when the search is being executed.
Support for long-living stream
Fortunately, we can leverage one RxJS operator to handle this kind of functionality — concat
.
concat subscribes to observables in order as previous completes
With this in mind let’s wrap our service call in concat
operator:
obs$ = this.searchStream$.pipe(
debounceTime(200),
distinctUntilChanged(),
switchMap((query) =>
concat(
// emit { type: 'start' } immediately
of({ type: 'start'}),
this.productsService.getByFilter(query)
// map to the wrapped object with type finish
.pipe(map(value => ({ type: 'finish', value })))
})
);
Cool, once a new event comes from input stream we immediately emit a new object which will indicate about starting the loading process. And as soon as we get the response from service we also wrap the result in another object with type finish
so that we can distinguish that our observable is resolved.
Now let’s change our custom WithLoadingPipe
a bit:
import { Pipe, PipeTransform } from '@angular/core';
import { isObservable, of } from 'rxjs';
import { map, startWith, catchError } from 'rxjs/operators';
@Pipe({
name: 'withLoading',
})
export class WithLoadingPipe implements PipeTransform {
transform(val) {
return isObservable(val)
? val.pipe(
map((value: any) => ({
loading: value.type === 'start',
value: value.type ? value.value : value
})),
startWith({ loading: true }),
catchError(error => of({ loading: false, error }))
)
: val;
}
}
We’ve changed only map
handler.
map((value: any) => ({
loading: value.type === 'start',
value: value.type ? value.value : value
})),
Now let’s check the HTML changes for ngIfElse solution and custom pipe:
<h2 class="title">Products</h2>
<div class="search-bar">
<input (input)="searchStream$.next($event.target.value)">
</div>
<div class="results">
<h3>Built-in solution</h3>
<div *ngIf="obs$ | async as obs">
<ng-template [ngIf]="obs.type === 'finish'">
{{obs.value}}
</ng-template>
<ng-template [ngIf]="obs.type === 'start'">Loading...</ng-template>
</div>
<h3>WithLoadingPipe</h3>
<div *ngIf="obs$ | withLoading | async as obs">
<ng-template [ngIf]="obs.value">{{ obs.value }}
</ng-template>
<ng-template [ngIf]="obs.loading">Loading...</ng-template>
</div>
</div>
With those changes in place we got a great products page:

As always, take a look at the full code in Ng-run example.
We can also use startWith operator here to get the same behavior. https://ng-run.com/edit/YeEFyf7DT9fk1H9E7vVZ but concat gives us more flexibility to control the state of our loader. Just imagine that we call several http requests in parallel and want to change the state of our loader after each of http call.
Thank you for reading!