DEV Community

Rens Jaspers
Rens Jaspers

Posted on • Updated on

Why Both Subscribe and *ngIf with Async Pipe Fall Short (and What You Should Use Instead)

Handling data loading from observables in Angular templates can be a tricky business. Two commonly suggested solutions - subscribing to the observable or using *ngIf with the async pipe - each come with their own issues.

The Subscription Problem

First, let's take a look at subscribing. Subscriptions are great, until they aren't. One key issue here is that it's all too easy to forget to unsubscribe, which can lead to memory leaks and a performance hit over time. Additionally, if you're using the OnPush change detection strategy, you need to manually trigger change detection. This is not only tedious, but also counter-intuitive and potentially error-prone.

import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Observable } from 'rxjs';

  selector: 'my-component',
  template: `<div>{{data}}</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush
export class MyComponent {
  data: any;

  constructor(private myService: MyService, private cd: ChangeDetectorRef) {}

  ngOnInit() {
    this.myService.getData().subscribe(newData => { = newData;
      // Whoops! Did you just forget to unsubscribe?
      // Memory leaks are sneaking in...

      // Uh-oh! Seems like you didn't there, buddy!
      // Your view isn't going to update with OnPush change detection.
Enter fullscreen mode Exit fullscreen mode

The Async Pipe Problem

Then there's *ngIf with async pipe. On the surface, it appears to be a more elegant solution. There's no need to handle subscribing and unsubscribing, and with the handy else syntax, you can display a loading template with ease. But in reality it isn’t that great.

Most blog posts or tutorials that suggest this method seldom touch upon error handling, which can complicate the template and detract from the elegance. Moreover, dealing with falsy values can also pose a challenge. Falsy values can inadvertently trigger the else block, which could lead to undesired effects.

<div *ngIf="count$ | async as count; else errorOrLoading">
  <p>{{count}} items</p>
  <!-- Whoops, this value is 0 and causing your loading template to show forever! -->

<ng-template #errorOrLoading>
  <!-- Hmm, doesn't look as elegant when you suddenly have to handle errors, does it? -->
  <div *ngIf="error; else loading">
    <p>Error occurred while loading data.</p>
  <ng-template #loading>
Enter fullscreen mode Exit fullscreen mode

So what should we use?

What we really need is a solution that takes the best of both worlds, manages observable subscriptions, automatically detects changes with OnPush, handles loading state gracefully, and is performant, declarative and minimalistic.

Unfortunately I couldn't find an existing solution that met all these criteria, so I created one.

Meet ngx-load-with

⚡️ Live Example

<ul *ngxLoadWith="todos$ as todos; loadingTemplate: loading; errorTemplate: error">
  <li *ngFor="let todo of todos">{{todo.title}}</li>
<ng-template #loading>Loading...</ng-template>
<ng-template #error let-error>{{error.message}}</ng-template>
Enter fullscreen mode Exit fullscreen mode
export class MyComponent {
  todos$ = inject(HttpClient).get<Todo[]>('api/todos');
Enter fullscreen mode Exit fullscreen mode

The *ngxLoadWith structural directive automatically manages subscriptions, gracefully handles loading and error states, and provides excellent performance with a minimal amount of code in both your TypeScript and your template.

Furthermore, ngx-load-with goes a step beyond to cater to some common use-cases, making loading handling in Angular a breeze.

You can find ngx-load-with on npm or check out the project on GitHub.

If you find it useful, please give it a star and feel free to contribute. I'd love to hear your feedback or any ideas for improvements. Leave a comment below and let's make data loading in Angular more efficient and elegant!

Top comments (0)