Angular: show loading indicator when obs$ | async is not yet resolved

Alexey Zuev
Angular In Depth
Published in
5 min readAug 28, 2019

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

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

  1. Let’s change our observable a bit so that it returns a falsy value:
obs$ = of(0).pipe(delay(500))

We’re stuck at this screen:

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.

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:

Using WithLoadinPipe

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:

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:

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.

Update

Typed version of this pipe can be found here https://ng-run.com/edit/TmKtfF2wZL5HBnSUoPQS?open=app%2Fobs-with-status.pipe.ts

Thank you for reading!

--

--