Home > database >  How to manage service member initialization with data retrieved through http request from another se
How to manage service member initialization with data retrieved through http request from another se

Time:02-19

I have a dataService that contains members that need to be initialized from .json that is retrieved via http. I have another httpService that makes the http request, and for now (until I understand what I'm doing), holds onto that data.

I was under the impression that the httpService would/could be set up to finish retrieving the data such that other services that injected it would be able to access the retrieved data as soon as their constructors were called, and use it to initialize their members.

I've learned that this isn't how it works, but am having a difficult time figuring out the right way to do something like this.

httpService

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class HttpService {
  private dataUrl = 'url to data';
  public retrievedData!: jsonData;

  constructor(private http: HttpClient) {
    this.getJsonData().subscribe(incJson => {
      this.retrievedData = incJson;
      console.log(this.retrievedData);  // logs the data as expected
    });
  }

  getJsonData(): Observable<jsonData> {
    return this.http.get<jsonData>(this.dataUrl);
  }
}

export interface jsonData {
  // shape of json data
}

dataService

import { Injectable } from '@angular/core';
import { dataService } from './data.service';

@Injectable({ providedIn: 'root' })
export class DataService {
  public member1: number;
  public member2: number;
  public member3: any;

  constructor(private httpService: HttpService) {
    // these values are all initialized as undefined
    this.member1 = this.httpService.retrievedData.someValue1;
    this.member2 = this.httpService.retrievedData.someValue2;
    this.member3 = this.httpService.retrievedData.someArray;
  }

  public doStuff() {
    console.log(this.member1);  // undefined

    for (let i = 0; i < this.member3.length; i  )  // throws an error
    {
        // do more stuff
    }

    ...
  }

Does the approach that I'm trying to use even make sense? Am I missing the mark completely? I understand that with the http requests I'm fetching the data asynchronously, but how do I manage other services that rely on this data?

CodePudding user response:

I guess that you are misunderstanding concepts/ways of doing, mixing the usual way of a "http" service ('getJsonData' method in your example) with its use in other component/service ('dataService' service in your case), that you are doing wrong in the httpService itself.

Meaning:

  1. In your http service, retrieve data only through the methods, not through the attributes as 'retrievedData'. So leave your httpService just with the methods that returns observables, like this:

HttpService:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

export interface jsonData {
  // shape of json data
}

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

  constructor(private http: HttpClient) {
  }

  getJsonData(): Observable<jsonData> {
    return this.http.get<jsonData>(this.dataUrl);
  }

}
  1. **Use that http service wherever you want to retrive the data: (in your example, in DataService)
import { Injectable } from '@angular/core';
import { dataService } from './data.service';

@Injectable({ providedIn: 'root' })
export class DataService {
  public member1: number;
  public member2: number;
  public member3: any;

  constructor(private httpService: HttpService) {

   this.httpService
   .getJsonData()
   .subscribe( (respJsonData:jsonData) => {

      if (respJsonData) {
         this.member1 = respJsonData.someValue1;
         this.member2 = respJsonData.someValue2;
         this.member3 = respJsonData.someArray;
      }

    });

  }

  public doStuff() {

    for (let i = 0; i < this.member3?.length; i  )  // Could throw an error if this.member3 has not been initialized yet
    {
        // do more stuff
    }
    ...
  }


  public doStuff2() {

    if (!this.member3) {return;}

    for (let i = 0; i < this.member3?.length; i  )
    {
        // do more stuff
    }
    ...
  }

  public doStuff3() {

    if (!this.member3) {
       const msg="Data is not loaded";
       alert(msg);
       return msg;
    }

    for (let i = 0; i < this.member3?.length; i  )
    {
        // do more stuff
    }
    ...
  }

EDIT: If you try to execute 'doStuff()' before this.member3 is initialized, it will fail: It's logical that a method that tries to access an attribute that has not yet been initialized will fail. If you want to avoid the error until it is initialized, just avoid the error, use "?." or an if before using the attribute (I edit it in my code and you see how to do it).

If you want something more elaborated, you should display a warning message saying that the data is not been loaded yet in that own method (or return the message to the method that invoke it), or have a boolean attribute that indicate if data is already retrieved in the constructor or not, and if it's not, not to execute methods that are going to access that retrieved data, etc.

CodePudding user response:

You can use a BehaviorSubject in your HTTP service to cache the request result and make it available to any subscribers (now or in the future):

HTTP Service:

@Injectable({ providedIn: 'root' })
export class HttpService {
  private dataUrl = 'url to data';
  public retrievedData!: jsonData;

  public dataSubject = new BehaviorSubject<jsonData>(null);
  public data$ = this.dataSubject.asObservable().pipe(
    filter(data => !!data));  // avoid need for if check on subscriber

  constructor(private http: HttpClient) {
    this.getJsonData().subscribe(incJson => {
      dataSubject.next(incJson);
    });
  }

  getJsonData() {
    return this.http.get<jsonData>(this.dataUrl);
  }
}

Data Service

Any service requiring the http data can inject HttpService then access the data via data$. No further checks are required as data$ will only emit complete response values.

@Injectable({ providedIn: 'root' })
export class DataService {
  constructor(private httpService: HttpService) {}

  public doStuff() {
    this.httpService.data$
     .subscribe((respJsonData:jsonData) => {
        // use respJsonData.member3 directly(or any other member), 
        // they are guaranteed to be initialized
        ...
     });

  public doStuff2() {
    this.httpService.data$
     .subscribe((respJsonData:jsonData) => {
        // use any respJsonData members 
        ...
     });
  }

  • Related