4

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.

o.o
  • 3,563
  • 9
  • 42
  • 71

4 Answers4

4

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.

cyberpirate92
  • 3,076
  • 4
  • 28
  • 46
2

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
      });
  }

}
ElasticCode
  • 7,311
  • 2
  • 34
  • 45
  • 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. – o.o May 02 '18 at 19:22
  • once you get it set the service variable to result data and use it directly, don't make two services – ElasticCode May 02 '18 at 19:25
  • 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? – o.o May 02 '18 at 19:28
  • 1
    Yes, but you can check if the service variable have data do not call the server – ElasticCode May 02 '18 at 19:32
1

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!!

BSchnitzel
  • 136
  • 2
  • 15
  • 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. – o.o May 02 '18 at 19:15
  • Ah, so it is failing in the test component above, correct? – BSchnitzel May 02 '18 at 19:17
  • 1
    Thanks, it also worked this way and it's always great to know different ways to do things. – o.o May 02 '18 at 20:24
  • 2
    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(). – martin8768 May 02 '18 at 20:25
0

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; 
     });
    }
}
Franklin Pious
  • 3,670
  • 3
  • 26
  • 30
  • Unfortunately, this put me in the same situation as before. Multiple calls to the rest service. – o.o May 02 '18 at 20:25