0

I'm fairly new to the Angular scene and I'm starting to pull my hair out of over some unexpected behaviour.

I have the following code in my IntegrationsService

queryList(): Observable<ApiList[]> {
    if(this.loadingApiList == true) {
      return Observable.of(this.queryListResult);
    }
    this.loadingApiList = true;
    return this._http
      .get('samples/apiList.json').map( (response: Response) => <ApiList[]> response.json())
      .do(data => {
        this.queryListResult = data;
      });
}

How can I stop this call being executed twice on page load from a route?

A component calls this method and builds a list in the navigation, also another component calls the same service for the following method:

getApiName(IT_ID:number) {
    console.log(this.queryListResult);
    let obj = this.queryListResult.find(o => o.IT_ID == IT_ID);
    if(obj) {
      return obj.IT_Name;
    }
    return false;
}

This works perfectly when I am not using routes, however when I use routes the getApiName returns an undefined result.

I used the resolve object too and that did resolve the problem but then two calls were being made to the json file, this is what I used.

  resolve(route: ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<any>{
    return this._service.queryList() // We need this to finish first.
  }

I'm really stuck now - I can get it working using the resolve but it's wasteful having two calls to the .json.

What is the correct way of doing this?

Lazar Ljubenović
  • 18,976
  • 10
  • 56
  • 91
Colin
  • 159
  • 8
  • Having set the flag doesn’t necessarily mean that the operation is complete and the query list result has been assigned. One method to achieve this is expose the data over a subject-based observable and trigger a fetch once, see e.g. https://blog.jonrshar.pe/2017/Apr/09/async-angular-data.html where I show how we’ve used this pattern. – jonrsharpe Sep 24 '17 at 11:25

1 Answers1

2

How can I stop this call being executed twice on page load from a route?

This is happening because you subscribe twice. Observables are lazy and each time you invoke one with .subscribe(), it will start executing: in your case, it will trigger a network request. Think of .subscribe() in observables as .call() or simply () for functions: every time you call a function, it will execute.

With that in mind, the behavior you encounter is expected. There are different ways to solve this, which are similar yet slightly differ based on your use-case; ie. the exact timing of subscribing and unsubscribing. In general, what you're looking for is called multicasting.

The simplest form of multicasting is using the .shared() operator. From the official docs; but note the emphasis (mine):

Returns a new Observable that multicasts (shares) the original Observable. As long as there is at least one Subscriber this Observable will be subscribed and emitting data. When all subscribers have unsubscribed it will unsubscribe from the source Observable. Because the Observable is multicasting it makes the stream hot. This is an alias for .publish().refCount().

What you might be interested is in creating a BehvaiorSubject, as it has the notion of "current value". Then your API call will not return anything, it will simply trigger the request.

Here's a proof-of-concept:

export class Service {

  public data$ = new BehaviorSubject(null);

  public get() {
    this.http.get(`some/data`).subscribe(data => this.data$.next(data))
  }

}

null is the value every subscriber will get synchronously when subscribed to data$. You can use it like this.

this.get()
this.data$.subscribe(data => /* ... */)

Of course, you can wrap this into something nicer, and even start implementing things like cache, etc.


Some useful links:

Lazar Ljubenović
  • 18,976
  • 10
  • 56
  • 91
  • Thank you for the detailed answer. Wouldn't using the BehaviorSubject cause the same issue with two requests being made due to each component calling the get() ? – Colin Sep 24 '17 at 11:45
  • True, that's why I said, the setup I've given here is just a starting point to understand the `BehaviorSubject`. The idea is that you should handle how often that endpoint can be called with custom logic. You do not want _never_ to call it again because data might get stale. From RxJS point of view, for all it knows, those two calls you make might be different results. It's up to you to somehow cache the response and determine for long the response is valid. – Lazar Ljubenović Sep 24 '17 at 11:46
  • Just to be clear, if I called the get() on the ngOnInit of the service to make sure the data is present. All components subscribed BEFORE and AFTER this get() would receive the updated copy of data when it is available using the subscribe? – Colin Sep 24 '17 at 12:11
  • 1
    Just tested it - you sir are a genius. Thank you for your help. – Colin Sep 24 '17 at 13:05