1

I have a HTTP service which returns some information when a given item ID is passed to it. This is done through a Subject, which received the first piece of data in then ngOnInit method.

I then use the async pipe to display the data returned by the service in the HTML.

My problem is that the async pipe hasn't subscribed to the observables at the point where I call selections.next with the first item ID - therefore this one isn't displayed on initialisation.

How can I wait until the async pipe has subscribed to the Observable before I send the first piece of data to the subject to begin the first HTTP request?

I've tried the different lifecycle hooks but none seem to have worked.

import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Subject } from "rxjs/Subject";

import { ExampleService } from "./example.service";

import "rxjs/add/operator/switchMap";

@Component({
  template: `
    <div>
      <div *ngFor="let time of times | async">{{ time }}</div>
    </div>
  `,
})
export class ExampleComponent implements OnInit {

  times: Observable<string[]>;

  constructor(
    private exampleService: ExampleService
  ) { }

  ngOnInit() {

    var itemIds = new Subject<number>();

    this.times = itemIds
      .switchMap(itemId => this.exampleService.getData(itemId))
      .map(data => this.calculateTimes(data));

    // Pass an item ID to the subject.
    // This is done periodically as well as
    // on init.
    itemIds.next(10);
  }

  calculateTimes(data: string[]) {
    /*
     * Some processing code.
    */

    return data;
  }
}
Kian Cross
  • 1,818
  • 2
  • 21
  • 41
  • Why do you have a service in a component? That complicates everything. – Prav Oct 27 '17 at 19:32
  • @PraveenM What's wrong with this? – Kian Cross Oct 27 '17 at 19:40
  • Nothing is wrong with that, it can complicate things sometimes. I think you should keep things like `Subjects` and `BehaviourSubjects` in service files. It's just my opinion. – Prav Oct 27 '17 at 19:44
  • sometimes you want something local to your component to trigger your service subscription, switchMap and a local subject is the most reactive way to accomplish this. Other structures may feel more comfortable if you're not used to higher order operators but this is better than having a function that calls subscribe itself since switchMap handles concurrency and subscription management automatically. – bryan60 Oct 27 '17 at 19:47

1 Answers1

4

Use a behavior subject instead of a subject.

the behavior subject saves it's last value and sends it to new subscribers on subscription.

import { BehaviorSubject } from "rxjs/BehaviorSubject";


var itemIds = new BehaviorSubject<number>(null);

The behavior subject needs to be initialized with a value. It exists to solve this timing problem when you don't know if your value or your subscriber will arrive first.

If you're trying to avoid dup calls, you can set up a local store pattern:

times: BehaviorSubject<string[]> = new BehaviorSubject<string[]>();


var itemIds = new Subject<number>();

itemIds.switchMap(itemId => this.exampleService.getData(itemId))
    .map(data => this.calculateTimes(data)).subscribe(this.times);

this way your only subscriber to the http call is your behavior subject that you subscribe to in template.

bryan60
  • 28,215
  • 4
  • 48
  • 65
  • I think his problem is HTML trying to read the value before values get there in the first place. He needs to organize his `components` and `services` better. – Prav Oct 27 '17 at 19:35
  • 1
    that isn't the issue, there is A. nothing wrong with subscribing to an observable before the value gets there (in template or otherwise) and B. async subscriptions happen after oninit... the issue is that he is triggering in onInit and the subscription doesn't happen till after he calls his "next" on the source subject. A behavior subject solves this. – bryan60 Oct 27 '17 at 19:37
  • That will start giving `null pointer exceptions` but that can be dealt with. In my opinion, I think he should move the `itemIds.switchMap` in a service, it will make it easy to deal with that. – Prav Oct 27 '17 at 19:41
  • no it won't. the async pipe is smarter than that. if an ngFor subscription gets a falsey value it treats it as an empty list. moving the operators might make sense or it might not, it depends on the goal. there isn't anything wrong with a structure of having a local subject trigger a switch to a service subscription. It happens all the time. – bryan60 Oct 27 '17 at 19:42
  • @bryan60 Thanks for this answer. My code example was simplified. In reality, I call `share` after the `switchMap` so I can use `map` and then subscribe using `async` from the template without sending multiple HTTP requests. Your answer works when I don't call `share`. I tried to simplify my code for the question - perhaps too much which is why I omitted the `share` call (didn't think it was relevant). Is it possible to use `BehaviorSubject` in this way? – Kian Cross Oct 27 '17 at 19:52
  • no i don't think that will work, i'll put up a structure that will – bryan60 Oct 27 '17 at 19:58
  • 1
    I put up a structure that will work, however, it shouldn't be up to your component to prevent duplicate http calls, that should be handled in the service level. Your component needs to know too much about the inner workings of the service right now. putting share on the service call MIGht work though, can't test it rn – bryan60 Oct 27 '17 at 20:05
  • @bryan60 How would the prevention of multiple HTTP request be handled in the service. The service just returns some data from the server after doing some processing. – Kian Cross Oct 27 '17 at 23:02
  • You set up an observable That rebroadcasts the last value to all subscribers as long as it has subscribers. That way it doesn’t matter which component is requesting the data, they all have the same data. Component A doesn’t have to be concerned with what component B is doing. – bryan60 Oct 28 '17 at 15:42