Grant Curell
Grant Curell

Reputation: 1753

Angular subscribe not behaving as expected - array is empty

I'm following the Angular tour of heroes examples and have constructed (I think) my version of the code identically, but am not receiving the behavior I expect.

My service

import { Injectable } from '@angular/core';
import { PORTS } from './mock-ports'
import { Port } from './port'
import { Observable, of } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})

export class UpdateportsService {

  private controller_url = '/gelante/getports/150013889525632'
  private controller_ip = 'http://localhost:8080'

  getPorts(): Observable<Port[]> {
    return this.http.get<Port[]>(this.controller_ip + this.controller_url)
  }

  constructor(private http: HttpClient) { }
}

myObserver (used for debugging)

const myObserver = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

getPorts (subscribes to the observable service)

// This is part of the same class as getPorts
ports: Port[] = [];

getPorts(): void {
    // To subscribe to an observable, you take the declared observable, which in
    // this case is getPorts (which returns an array of ports) and then you
    // subscribe to it. Anytime the observable is called it will emit values
    // which are then sent to all subscribers.
    console.log(this.ports)
    this.updateportsService.getPorts().subscribe(ports => this.ports = ports);

    // This prints all of my port data as expected
    this.updateportsService.getPorts().subscribe(myObserver);

    console.log(this.ports)
  }

Full output from Debug Console

Array(0) []
switch.component.ts:76
Array(0) []
switch.component.ts:82
Angular is running in the development mode. Call enableProdMode() to enable the production mode.
core.js:40917
Observer got a next value: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]
switch.component.ts:13
Observer got a complete notification
switch.component.ts:15
[WDS] Live Reloading enabled.

Goal

The goal is to take a listing of switch interfaces I'm receiving from a REST API (separate from Angular) and assign them to a list of dictionaries called ports. This should be accomplished in the line:

this.updateportsService.getPorts().subscribe(ports => this.ports = ports);

Problem

In the tour of heroes example ports in the function getPorts should be populated. I have confirmed from both Wireshark and some debug output that the HTTP get request is functioning as expected. Specifically, you can see the line:

this.updateportsService.getPorts().subscribe(myObserver);

That it receives a big array of objects (as expected). However, for whatever reason the assignment in ports => this.ports = ports does not seem to work. The value of ports is always an empty array with zero elements. However, I haven't been able to figure out why.

Upvotes: 0

Views: 2390

Answers (3)

Barremian
Barremian

Reputation: 31105

This is a simple case of trying to access asynchronous data before it is assigned a value. In this case, this.ports is assigned asynchronously. So by the time you do console.log(this.ports), it isn't assigned any value. But when you use myObserver it works because you are printing inside the subscription, as it's supposed to be. The exact equivalent using ports would be the following

this.updateportsService.getPorts().subscribe(
  ports => { 
    this.ports = ports;
    console.log(this.ports);
  },
  error => {
     // it is always good practice to handle error when subscribing to HTTP observables
  }
);

See here to learn more about asynchronous requests.

async pipe vs subscription in the controller

async pipe is recommended in most cases because it takes care of unsubscribing from the observables so as to avoid memory leak issues. When subscribing manually to an observable, it is better to unsubscribe from it in the OnDestroy hook.

import { Subscription } from 'rxjs';

export class AppComponent implements OnInit, OnDestroy {
  obsSubscription: Subscription;

  ngOnInit() {
    this.obsSubscription = this.service.observable.subscribe(value => { // handle value });
  }

  ngOnDestroy() {
    if (this.obsSubscription) {
      this.obsSubscription.unsubscribe();
    }
  }
}

Usually the unsubscribe is overlooked when using the HttpClient because it handles the unsubscription and avoids memory leaks. However there are exceptions. For eg., if the user navigates away from the link that made the HTTP call, it might still be open. So it is always recommended to close the subscription manually.

There is also an another elegant way of handling the unsubscription using takeUntil.

import { Subject, pipe } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

export class AppComponent implements OnInit, OnDestroy {
  closeActive = new Subject<void>();

  ngOnInit() {
    this.obsSubscription = this.service.observable
      .pipe(takeUntil(this.closeActive))
      .subscribe(value => { // handle value });
  }

  ngOnDestroy() {
    this.closeActive.next();
    this.closeActive.complete();
  }
}

Upvotes: 5

J&#243;zef Podlecki
J&#243;zef Podlecki

Reputation: 11283

That it receives a big array of objects (as expected). However, for whatever reason the assignment in ports => this.ports = ports does not seem to work. The value of ports is always an empty array with zero elements. However, I haven't been able to figure out why.

Well you consumed your observable with subscribe(myObserver);

You can either use pipe and tap as @user3791775 suggested

or extend your observer and assign value there.

const myObserver = {
  next: ports => {
    this.ports = ports;
    console.log('Observer got a next value: ' + ports)
  },
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

--Edit

Actually there is another solution

You can create Subject which allows you to handle multiple subscriptions.

getPorts(): Subject<Port[]> {
    const subject = new Subject<Port[]>();
    return this.http.get<Port[]>(this.controller_ip + this.controller_url).subscribe(ports => subject.next(ports));
    return subject;
  }

Upvotes: 0

user3791775
user3791775

Reputation: 471

I don't understand your observer debug, but I think you should do it more like this (you should try to avoid manual subscription and use the async pipe but that's not your question):

Lose the debug observer and inside your getPorts method do this:

this.updateportsService.getPorts().pipe(
 tap(ports => console.log(ports),
 tap(ports => this.ports = ports),
 catchError(e => console.log(e)) // I suspect you'll catch some kind of error
).subscribe()

hope this helps to debug

Upvotes: 0

Related Questions