Search code examples
angularhttpnativescriptrxjs-observables

Angular - would observables benefit me in a basic HTTP POST request vs promises?


I'm writing an app with Angular 8. I'm kind of new to the framework coming from AngularJS. I have a lot of experience with Promises.

I have HTTP requests to my backend API. These are just GET & POST calls as there aren't any WebSockets in our Delphi backend. I have successfully called out backend API and used promises.

My code calls the backend API to get a string value that I want to display to the user.

I'm wondering how I can refactor my code to use Observables and if there is any benefit in doing so?

Service

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ILicenseInfo } from './ilicense-info';

@Injectable({
    providedIn: 'root'
})
export class AboutService {

    constructor(private http: HttpClient) { }

    public getVersionNumber() {

        const endpoint: string = 'myEndpoint';

        const params = {
            password: 'yes'
        };

        return this.http.post<ILicenseInfo>(endpoint, params)
            .toPromise()
            .then((response: ILicenseInfo) => {
                return response.versionNumberField;
            });

    }

}

Component

import { Component, OnInit } from '@angular/core';
import { Constants } from '../../../../app/core/constants/constants.service';
import { AboutService } from './about.service';

@Component({
    selector: 'ns-about',
    templateUrl: './about.component.html',
    styleUrls: ['./about.component.css']
})
export class AboutComponent implements OnInit {

    public version: string;

    constructor(private aboutService: AboutService) { }

    public ngOnInit(): void {

        this.aboutService.getVersionNumber().then((response: string) => {
            this.version = response;
        });

    }

}

Template

<FlexboxLayout>

    <FlexboxLayout>
        <Label text="Version:"></Label>
        <Label text="{{version}}"></Label>
    </FlexboxLayout>

</FlexboxLayout>

Solution

  • RxJS provides many operators and methods to modify and control the data flow from the source obsevable. More detailed list here.

    Eg.

    Combine two requests

    forkJoin([this.http.get('url'), this.http.post('url', '')]).subscribe
      resposne => {
        // resonse[0] - GET request response
        // resonse[1] - POST request response
      },
      error => {
      }
    );
    

    Pipe a response from one call as input to another request

    this.http.get('url').pipe(
      switchMap(getResponse => {
        return this.http.post('url', getResponse)
      }
    ).subscribe(
      response => { },
      error => { }
    );
    

    These are some of the most mundane advantages. RxJS would be more useful when multiple co-dependent observables need to handled.


    In your case you could pipe in the map operator to return the versionNumberField property from the response.

    return this.http.post<ILicenseInfo>(endpoint, params).pipe(
      map(response: ILicenseInfo => response.versionNumberField)
    );
    

    You could then subscribe to it the component to retrieve the emitted values

    public ngOnInit(): void {
      this.aboutService.getVersionNumber().subscribe(
        (response: string) => {
          this.version = response;
        }
      );
    }
    

    Potential memory leaks

    With the observables, there comes a risk of potential memory leaks from non-closed subscriptions. The Angular HTTP client has a built-in unsubscription mechanism, but that too can fail. So it is always better to close the subscription in the component (usually in the ngOnDestroy() hook).

    import { Subscription } from 'rxjs';
    
    httpSubscription: Subscription;
    
    public ngOnInit(): void {
      this.httpSubscription = this.aboutService.getVersionNumber().subscribe(
        (response: string) => {
          this.version = response;
        }
      );
    }
    
    ngOnDestroy() {
      if (this.httpSubscription) {
        this.httpSubscription.unsubscribe();
      }
    }
    

    There is also an elegant way of handling unsubscription using RxJS takeUntil operator.

    import { Subject } from 'rxjs';
    import { takeUntil } from 'rxjs/operators';
    
    closed = new Subject<void>();
    
    public ngOnInit(): void {
      this.httpSubscription = this.aboutService.getVersionNumber().pipe(
        takeUntil(this.closed)
      ).subscribe(
        (response: string) => {
          this.version = response;
        }
      );
    }
    
    ngOnDestroy() {
      this.closed.next();
      this.closed.complete();
    }
    

    So the susbcription will be active until the closed observable is open.

    But both these methods can get tedious very quick if you're handling multiple observables in multiple components. There is a clever workaround for this sourced from here.

    First create the following export function

    // Based on https://www.npmjs.com/package/ng2-rx-componentdestroyed
    // Source credit: https://stackoverflow.com/a/45709120/6513921
    
    import { OnDestroy } from '@angular/core';
    import { ReplaySubject, Observable } from 'rxjs/ReplaySubject';
    
    export function componentDestroyed(component: OnDestroy): Observable<void> {
      const oldNgOnDestroy = component.ngOnDestroy;
      const destroyed$ = new ReplaySubject<void>(1);
      component.ngOnDestroy = () => {
        oldNgOnDestroy.apply(component);
        destroyed$.next(undefined);
        destroyed$.complete();
      };
      return destroyed$.asObservable();
    }
    

    Now all there is to do is import the function, implement ngOnDestroy hook in the component and pipe in takeUntil(componentDestroyed(this) to the source observable.

    import { takeUntil } from 'rxjs/operators';
    
    public ngOnInit(): void {
      this.httpSubscription = this.aboutService.getVersionNumber().pipe(
        takeUntil(componentDestroyed(this))       // <-- pipe in the function here
      ).subscribe(
        (response: string) => {
          this.version = response;
        }
      );
    }
    
    ngOnDestroy() { }      // <-- should be implemented
    

    Update: async pipe

    The async pipe can also be used to retrieve values asynchronously. It works with both observables and promises. When used with observables, it makes sure the subscription is always closed when the component is destroyed without any modifications in the controller.

    Controller

    version$: Observable<any>;
    
    public ngOnInit(): void {
      this.version$ = this.aboutService.getVersionNumber();
    }
    

    Template

    <ng-container *ngIf="(version$ | async) as version">
      Version is {{ version }}
      <ng-container *ngIf="version > 5">
        It is already the latest version available.
      <ng-container>
    </ng-container>
    

    Variable version$ is named with the common convention of suffixing a dollar sign to Observable types.