Creating a loading indicator using RxJs and the withLoading pattern

Tomasz Wierzbicki
The Aize Employee Blog
9 min readMar 17, 2023

--

Every app will, sooner or later, need some kind of a progress/loading indicator to be displayed whenever a longer-running operation occurs, so that the users get proper feedback. A typical case would be non-trivial CRUD operations on a large or not optimised dataset.

In this post we will look at different approaches to how we can implement a simple loading indicator using RxJs.

Through this exercise, we will be using a fork of the (in)famous angular-tour-of-heroes repo.

The spinner component

First of all, we need a visual component to be displayed when such a long-running operation occurs. This might, of course, simply be a text, like “Loading…” (remember the first version of gmail?). The most common approach, however, is to use a progress bar or a spinner. Since we’ll be using Angular for the rest of this article, let’s go ahead and create a simple, css-only, single-file spinner component, based on this implementation.

@Component({
selector: 'app-spinner',
template: `<div class="spinner-container"><div class="spinner"></div></div>`,
styles: [
`
.spinner-container {
padding: 32px 0;
}
.spinner {
border: 8px solid #f3f3f3;
border-radius: 50%;
border-top: 8px solid #3498db;
width: 80px;
height: 80px;
animation: spin 2s linear infinite;
margin: auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`,
],
})
export class SpinnerComponent {
}

Promises, promises

Now, before we dive into the magical world of RxJs, let’s see how we would utilise our spinner in a classic, promise based scenario. For this purpose, we will add a new component to our Tour of Heroes app, where we will use the fetch api to retrieve a random cat fact.

Why not use one of the endpoints already existing in the app, you might ask? The reason is that we mock the backend, using the angular-in-memory-web-api , so we are not sending out actual requests, which means we cannot use the fetch api for those endpoints.

@Component({
selector: 'app-catfact',
template: `
<div style="height: 200px; padding: 50px">
<app-spinner *ngIf="isLoadingCatFact"></app-spinner>
<span *ngIf="!isLoadingCatFact && catFact">{{ catFact }}</span>
</div>
`,
})
export class CatFactComponent implements OnInit {
isLoadingCatFact = false;
catFact?: string;

constructor() {}

ngOnInit(): void {
this.fetchCatFacts();
}

async fetchCatFacts() {
let timeout;

clearTimeout(timeout);
this.isLoadingCatFact = true;

timeout = setTimeout(async () => {
const response = await fetch('https://catfact.ninja/fact');
const data = await response.json();

this.isLoadingCatFact = false;
this.catFact = data.fact;
}, 1000);
}
}

As we see, there is nothing fancy or new here, we set the isLoadingCatFact boolean property to true before we send out the request, then we set it back to false when we have received the response.

The reactive approach

Now, let’s try to implement a similar behaviour with RxJs. For this purpose, let’s add a new component to our app. This is going to be a copy of the dashboard component (minus the search option). We will also need a new route for the component.

First let’s create the component’s service:

@Injectable()
export class TopHeroesWithTapLoadingService {
private _isLoadingHeroes$ = new BehaviorSubject(false);
isLoadingHeroes$ = this._isLoadingHeroes$.asObservable();
topHeroes$!: Observable<Hero[]>;

constructor(private heroService: HeroService) {
this.topHeroes$ = this.getTopHeroes();
}

getTopHeroes(): Observable<Hero[]> {
this._isLoadingHeroes$.next(true);

return this.heroService.getHeroes().pipe(
map((heroes) => heroes.slice(0, 5)),
tap(() => this._isLoadingHeroes$.next(false)),
);
}
}

And the component:

@Component({
selector: 'app-top-heroes-with-tap-loading',
template: `
<app-spinner *ngIf="service.isLoadingHeroes$ | async"></app-spinner>
<div class="heroes-menu" *ngIf="service.topHeroes$ | async as heroes">
<a *ngFor="let hero of heroes" routerLink="/detail/{{ hero.id }}" class="heroes-menu__item">
{{ hero.name }}
</a>
</div>
`,
styles: [`
...type
`],
providers: [TopHeroesWithTapLoadingService]
})
export class TopHeroesWithTapLoadingComponent {
constructor(public service: TopHeroesWithTapLoadingService) {}
}

I bet that the first thing that came to your mind when you read the title of this article was “well, can’t this be done with a BehaviorSubject? And you are right, it can! Just like we can see in the example above. We are setting the value of the isLoadingHeroes$ BehaviorSubject to true before returning the observable, which, when subscribed to, emits the retrieved heroes and sets isLoadingHeroes$ back to false. And indeed when we reload the app and navigate to the “Top heroes, Tap loading” tab, we can first see the spinner and then the list of our top heroes.

Note that in order to artificially slow down the “heroes” response, we needed to provide the delay option to the configuration of the HttpClientInMemoryWebApiModule (in app.module):

@NgModule({
imports: [
...
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false, delay: 1000 }
)
],
...
})

Also note that the service is not provided “in root”, but rather directly in the component decorator. This is done in order to not make it singleton. The problem with having the service as singleton here, is that the spinner would not be shown on subsequent navigations to the component. This is because the isLoadingHeroes$ emits true only once, when the service is instantiated, which would happen only the first time the component is created.

The reactive approach with a singleton service

Sometimes, however, it is desired to use the singleton pattern for the service providing our data, so let’s see how we could implement the same behaviour in such scenario.

In order to do that, let’s try to move the code where we are emitting true from isLoadingHeroes$ into our observable, so that it happens on each subsequent subscription. Let’s go ahead and create yet another copy of the DashboardComponent and yet another dedicated service (the component template looks exactly the same as in TopHeroesWithTapLoadingComponent so let’s only look at the service):

@Injectable({providedIn: 'root'})
export class TopHeroesWithTapLoading2Service {
private _isLoadingHeroes$ = new BehaviorSubject(false);
isLoadingHeroes$ = this._isLoadingHeroes$.asObservable();
topHeroes$: Observable<Hero[]>;

constructor(private heroService: HeroService) {
this.topHeroes$ = of(undefined).pipe(
tap(() => {
this._isLoadingHeroes$.next(true);
}),
switchMap(() => this.heroService.getHeroes()),
map((heroes) => heroes.slice(1, 5)),
tap(() => {
this._isLoadingHeroes$.next(false);
})
);
}
}

In order to wrap all side effects inside our observable, we use a tiny trick. We create a new observable with the of operator, emit true from isLoadingHeroes$ using the tap operator and then switchMap to heroService.getHeroes (). When we receive an emit from heroService.getHeroes(), we use the tap operator again to emit false from isLoadingHeroes$. All in the same pipe.

Let’s reload the app and navigate to Top heroes, Tap loading 2 to check if this approach works.

Well, unfortunately, it doesn’t. We don’t see a spinner when waiting for our heroes list. And if we open the console, we see the following exception:

A quick google search gives us the following explanation: https://angular.io/errors/NG0100.

So why did we get this error? The reason is that the initial value of isLoadingHeroes$ (false), used for the initial change detection, is changed as soon as the topHeroes$ observable is subscribed to, which happens before the initial change detection completes. In other words, two different values of isLoadingHeroes$ are emitted during the same change detection process. This can be confirmed when we add some logs to the component controller:

export class TopHeroesWithTapLoading2Component implements OnInit, AfterViewChecked {
constructor(public service: TopHeroesWithTapLoading2Service) {
}

ngAfterViewChecked(): void {
console.log('View checked');
}

ngOnInit(): void {
this.service.isLoadingHeroes$.subscribe(isLoadingHeroes => console.log('isLoadingHeroes', isLoadingHeroes));
}
}

Reloading the app gives the following logs in the console:

As usual, there is a quick fix we can apply in order to get rid of this issue. Let’s add a delay (of 0 ms) to our pipe before we set isLoadingHeroes$ to true.

this.topHeroes$ = of(undefined).pipe(
delay(0),
tap(() => {
this._isLoadingHeroes$.next(true);
}),
switchMap(() => this.heroService.getHeroes()),
map((heroes) => heroes.slice(1, 5)),
tap(() => {
this._isLoadingHeroes$.next(false);
})
);

Now, when the app is reloaded, we can see the console error is gone and the spinner appears until the list of heroes is emitted. Magic? No. By adding the delay operator with value 0 to our observable, we move the operation of emitting true from isLoadingHeroes$ to the end of the message queue, thus giving the initial change detection the time it needs to complete before it needs to run again.

This is confirmed by our console logs:

The spinner is also shown when navigating to another tab and then back again, despite the service being singleton.

All good then, right?

Well, kind of. Using side effects with tap is generally not considered best practice, as it makes the observables impure. Adding a delay on top of it, just to make sure that we don’t mess up Angular’s change detection, surely does not make it a great solution either.

That is when we come to the “withLoading” pattern, which fixes all the disadvantages of using a BehaviorSubject for controlling loading indicators.

The “withLoading” pattern

The idea behind this pattern is that the value emitted by the observable is wrapped in an object containing two properties:

  • value — the actual emitted value
  • isLoading — a boolean which indicates if the value is being retrieved.

The crucial part of the pattern is RxJs’s startWith operator. We use it to emit our wrapper object with the isLoading property set to true as soon as the observable is subscribed to. When the actual value is ready to be emitted (f.ex. we got a response from the api), we emit it inside our wrapper with the isLoading property set to false.

Let’s implement this pattern in the third (and last) copy of the dashboard component.

Let’s first look at the service:

export type LoadedValue<T> = {
isLoading: boolean;
value?: T;
};

@Injectable()
export class TopHeroesWithLoadingPatternService {
topHeroes$: Observable<LoadedValue<Hero[]>>;

constructor(heroService: HeroService) {
this.topHeroes$ = heroService.getHeroes().pipe(
map((heroes) => heroes.slice(0, 5)),
map(topHeroes => ({
isLoading: false,
value: topHeroes
})),
startWith({isLoading: true}),
)
}
}

And the component:

@Component({
selector: 'app-top-heroes-with-loading-pattern',
template: `
<ng-container *ngIf="service.topHeroes$ | async as loadedHeroes">
<app-spinner *ngIf="loadedHeroes.isLoading"></app-spinner>
<div class="heroes-menu" *ngIf="loadedHeroes.value as heroes">
<a *ngFor="let hero of heroes" routerLink="/detail/{{ hero.id }}" class="heroes-menu__item">
{{ hero.name }}
</a>
</div>
</ng-container>
`,
styles: [
`
...
`,
],
providers: [TopHeroesWithLoadingPatternService]
})
export class TopHeroesWithLoadingPatternComponent {
constructor(public service: TopHeroesWithLoadingPatternService) {}
}

Now let’s reload the app once again and navigate to ‘Top Heroes, WithLoading pattern’. We can see that we get the exact same result as before, with the spinner shown when heroes are being retrieved, but this time we didn’t use any BehaviorSubject!

This is smooth, I want to have it in all my observables now — (hopefully) you might say. There is, however, one last thing you might consider before you actually start applying it to your services.

As you apply the pattern to more and more observables in your code it gets obvious that there is some boilerplate involved (wrapping the “ready” value, using the startWith operator with the exact same argument). What we can do to reduce the boilerplate to minimum, is to create a base class, which we can decorate our services with:

export type LoadedValue<T> = {
isLoading: boolean;
error?: Error;
value?: T;
};

export class WithLoading {
getLoadingValue<T>(): LoadedValue<T> {
return { isLoading: true } as LoadedValue<T>;
}

buildReadyValue<T>(value: T): LoadedValue<T> {
return {
isLoading: false,
value
};
}

startWithLoading<T>(): OperatorFunction<LoadedValue<T>, LoadedValue<T>> {
return startWith(this.getLoadingValue<T>());
}

catchLoadingError<T>(): OperatorFunction<LoadedValue<T>, LoadedValue<T>> {
return catchError(error => {
console.error('ERROR:', error);
return of({ isLoading: false, error });
});
}
}

The decorated service would then look like:

@Injectable()
export class TopHeroesDecoratedWithLoadingPatternService extends WithLoading {
topHeroes$: Observable<LoadedValue<Hero[]>>;


constructor(heroService: HeroService) {
super();

this.topHeroes$ = heroService.getHeroes().pipe(
map((heroes) => heroes.slice(0, 5)),
map(topHeroes => this.buildReadyValue(topHeroes)),
this.startWithLoading(),
this.catchLoadingError()
)
}
}

Looking better? I sure think so! As a bonus, we add functionality to handle errors gracefully. In case of an exception, the emitted wrapper object will include an error property containing the actual thrown exception, so that it can be handled in our subscription. In the implementation above, the exception will additionally be logged to the console (in production code you would replace that with a proper error-logging mechanism).

So there you have it — the withLoading pattern powered by RxJs magic. Now let’s get rid of those BehaviorSubjects from our code, shall we?

Full code from this post can be found here: https://github.com/Aize-Public/tour-of-heroes-with-loading-pattern

--

--