Angular5 HttpClient, set variables after GET api call
Asked Answered
D

4

5

I have a service (lets call it MySharedService) which multiple components use. Inside MySharedService, I call another service that makes API calls. MySharedService holds a JavaScript object that is assigned after my GET call.

My problem is that my components rely on that JavaScript object to set their values in their constructors. How can I set their values when that JavaScript might be undefined because the API call has not completed yet? Here's a sample:

ApiService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
/* Other imports */

@Injectable()
export class ApiService {

    constructor(private http: HttpClient) { }

    getTestData(): Observable<MyDataInterface> {
        const API_URL = 'some_url_here';
        return this.http.get<MyDataInterface>(API_URL, { observe: 'response' });
    }
}

MySharedService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
/* Other imports */

@Injectable()
export class MySharedService {

    myData: MyDataInterface;

    constructor(private apiServie: ApiService) {
        this.apiService.getTestData().subscribe((response) => {
            this.myData = { ...response.body };
            console.log(this.myData);
        });
    }
}

TestComponent

import { Component, OnInit } from '@angular/core';
import { MySharedService } from '../../../services/myshared.service';
/* Other imports */    

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

    name: string;
    /* Other variables here. */

    constructor(private mySharedService: MySharedService) {
        // Error here because myData has not been populated, yet.
        this.name = this.mySharedService.myData.name;
    }
}

So the problem happens inside my components when I try to access data from the myData object because it hasn't been populated, yet (the console.log() does eventually print the data out after a couple of seconds). How am I supposed to go about getting the values? I only want to call the rest service once and save the object inside MySharedService then have all my components use that object.

Discontented answered 2/5, 2018 at 18:49 Comment(0)
R
5

You should be using a BehaviorSubject

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
/* Other imports */

@Injectable()
export class MySharedService {

    myData: BehaviorSubject<MyDataInterface> = new BehaviorSubject(null);

    constructor(private apiServie: ApiService) {
        this.apiService.getTestData().subscribe((response) => {
            this.myData.next({ ...response.body });
            console.log(this.myData.getValue());
        });
    }
}

It's a bad practice to do subscriptions in component constructors, use the ngOnInit lifecycle hook instead.

export class TestComponent implements OnInit {

    name: string;
    /* Other variables here. */

    constructor(private mySharedService: MySharedService) {
    }

    ngOnInit() {
        this.mySharedService.myData.subscribe((data: MyDataInterface) => { 
            if (data)
                this.name = data.name;
        });
    }
}

The network call will be made only once and the data will be cached in the BehaviorSubject, which all the components will be subscribed to.

Now Why use a BehaviorSubject instead of an Observable? Because,

  • Upon subscription BehaviorSubject returns the last value whereas A regular observable only triggers when it receives an onnext.

  • If you want to retrieve the last value of the BehaviorSubject in a non-observable code (without a subscription), you can use the getValue() method.

Recor answered 2/5, 2018 at 20:0 Comment(1)
Thank you! This seems to be the best way to do it for my case.Discontented
A
3

Inside your TestComponent when calling the service jus subscribe to as below, also call it in ngOnInit event

ngOnInit() {

  if (this.mySharedService.myData) {
    this.name = this.mySharedService.myData.name;
  } else {
    this.apiService.getTestData().subscribe(
      data => {
        // On Success
        this.mySharedService.myData = data.body.myData;
        this.name = this.mySharedService.myData.name;
      },
      err => {
        // On Error
      });
  }

}
Apathy answered 2/5, 2018 at 19:20 Comment(4)
Wouldn't that make multiple rest calls to the api if I had to that in every component that needs that data? I was trying to prevent multiple calls and only do it once.Discontented
once you get it set the service variable to result data and use it directly, don't make two servicesApathy
I might not be fully understanding. Even if I made one service, I have multiple components loading at a time, if I put the above code in each component, it will make multiple calls to the endpoint wouldn't it?Discontented
Yes, but you can check if the service variable have data do not call the serverApathy
B
2

I make it a standard practice to handle nulls/undefined coming from any kind of pub/sub

MySharedService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
/* Other imports */

@Injectable()
export class MySharedService {

public message = new Subject<any>();

myData: MyDataInterface;

constructor(private apiServie: ApiService) {
    this.apiService.getTestData().subscribe((response) => {
        if(response){
           this.myData = { ...response.body };
           this.sendUpdate(this.myData);
           console.log(this.myData);
        }else console.log("I guess you could do some sort of handling in the else")
    });


    sendUpdate(message) {
        this.message.next(message);
    }

    getUpdate(): Observable<any> {
    return this.message.asObservable();
    }

    clearUpdate() {
        this.message.next();
    }

}

TestComponent

import { Component, OnInit } from '@angular/core';
import { MySharedService } from '../../../services/myshared.service';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/takeWhile';
/* Other imports */    

@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.css']
})
export class TestComponent implements OnInit, OnDestroy {

subscription: Subscription;
private alive: boolean = true;
name: string;
/* Other variables here. */

constructor(private mySharedService: MySharedService) {
    // Error here because myData has not been populated, yet.
    this.subscription = this.mySharedService.getUpdate().takeWhile(() => this.alive).subscribe(message => {
            if (message) {
               this.name = message;
            }
        });
    this.name = this.mySharedService.myData.name;
   }
}
    //Best practice to clean up observables
    ngOnDestroy() {
    this.updateService.clearUpdate();
    this.subscription.unsubscribe();
    this.alive = false;
}

The edits I made take full advantage of a modern Angular service using Rxjs subscription functionality IMO. Over the last year I have been splitting time between

A) using stores with Flux/Redux/Mobx to pass resolved data around the application. Wait for the data to be resolved with observables or use Select methods to get data

or

B) using a pub/sub pattern to get updates from services when they have the actual data you need. I find it easier to use and comes in handy for smaller apps or plugging in directives and components here and there.

The key I think in your type of situation is to handle where there is data and also when there isn't. Good example of this is that "...loading" text that Angular pre-populates the view of every component you create using Angular CLI.

Just remember if you are going to use observables to clean them up using ngOnDestroy (Or unintended behavior can happen)

Good Luck!!

Bogor answered 2/5, 2018 at 19:11 Comment(4)
This ends up giving me the same scenario. My components will throw errors in their respective constructors when I try to access myData because myData has not been populated. Not sure what I could do in that else statement. Even if I set it to an empty object, the variables in the components will need to get updated somehow.Discontented
Ah, so it is failing in the test component above, correct?Bogor
Thanks, it also worked this way and it's always great to know different ways to do things.Discontented
I'd just add that the observable catch operator is very handy for catching errors and is much better for handling them so you don't have to do if (thing) else handleError().Picul
M
1

Try this and tell me.

MySharedService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
/* Other imports */

@Injectable()
export class MySharedService {

    myData: MyDataInterface;

    constructor(public apiService: ApiService) {
    }

    fetchData() {
       return this.apiService.getTestData();
    }

}

TestComponent

import { Component, OnInit } from '@angular/core';
import { MySharedService } from '../../../services/myshared.service';
/* Other imports */    

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

    name: string;
    /* Other variables here. */

    constructor(private mySharedService: MySharedService) {
        this.mySharedService.fetchData().subscribe((resp: any) => {
        this.name = resp.body; 
     });
    }
}
Mesmerism answered 2/5, 2018 at 19:22 Comment(1)
Unfortunately, this put me in the same situation as before. Multiple calls to the rest service.Discontented

© 2022 - 2024 — McMap. All rights reserved.