0

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.

R. Richards
  • 24,603
  • 10
  • 64
  • 64
Grant Curell
  • 1,321
  • 2
  • 16
  • 32
  • 1
    It’s a bit unclear what you’re trying to accomplish. If you want to test your subscribe, just do .subscribe(value => console.log(value)) – MikeOne Apr 30 '20 at 22:02
  • 1
    Am trying now. Updating my question for clarity. – Grant Curell Apr 30 '20 at 22:02
  • @MikeOne added a goal section. Does that better clarify? – Grant Curell Apr 30 '20 at 22:04
  • Huh, I did your trick @MikeOne (thanks for that!) and it works now... but I don't understand the difference. I changed the syntax in getPorts to: this.updateportsService.getPorts().subscribe(ports => { console.log(ports); this.ports = ports console.log(this.ports) }); It now correctly prints out all the ports received. As far as I can tell the only real difference is the presence of brackets. I can't tell if I've just been looking at this too long and am going crazy or if I'm missing something. – Grant Curell Apr 30 '20 at 22:10
  • Sometimes you just can’t see it anymore. Just simplify and break it down in smaller pieces. Good luck man. – MikeOne Apr 30 '20 at 22:13

3 Answers3

5

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();
  }
}
ruth
  • 29,535
  • 4
  • 30
  • 57
0

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

user3791775
  • 426
  • 3
  • 5
  • I have done a lot of programming, but I'm brand new to webdev/Angular. Why is using the async pipe a better design? (Not a challenge tone, I just don't understand hahaha) – Grant Curell Apr 30 '20 at 22:11
  • It's best practice in Angular, mainly because you don't have to unsubscribe manually (if you forget that, you get memory leaks) and it's a more reactive way of programming (piping data to your template, transforming it using rxjs) without using scope variables). – user3791775 Apr 30 '20 at 22:15
0

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;
  }
Józef Podlecki
  • 10,453
  • 5
  • 24
  • 50