1

I am working on a front-end project with Rxjs and Angular Framework and I want to get json data from a api "api/data_processor_classlib.php....". There are three parts was subscribed the pipe this.webProtectionHTML$ at HTML. I don't know why the pipe this.webProtectionHTML$ made requests 3 times. Is any possible solution that just sent one request and update all data in HTML? Thanks.

HTML Code:

    <div class="tr">
      <div class="align-left">Phishing & Other Frauds</div>
      <div class="align-right">{{ (webProtectionHTML$|async)?.phishing}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Spam URLs</div>
      <div class="align-right">{{ (webProtectionHTML$|async)?.spamURLs}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Malware Sites</div>
      <div class="align-right">{{ (webProtectionHTML$|async)?.malware}}</div>
    </div>

Component:

this.webProtectionHTML$ = this.dayService$
      .pipe(
        mergeMap((days: DaysPeriod // params added to request url) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
        map((html: string) => {
          //get html code and find data return as json data
          let result = this.getWebProtectionData(html)
          return result
        }))

Network log:

enter image description here

Di Wang
  • 43
  • 8

5 Answers5

5

A few previous answers correctly point out that each usage of async pipe results in a http request - but don't explain why. In fact it's not an issue with the async pipe per se. It's because your http request observable is "cold" (as explained e.g. here: https://www.learnrxjs.io/learn-rxjs/concepts/rxjs-primer#what-is-an-observable).

"Cold" observable means it will only start doing smth and emitting values when some consumer subscribes to it. Moreover, by default each new subscription initiates new execution - even if multiple subscriptions to the same observable are created in parallel. This is exactly what you observed in your code: each async pipe subscribes to the observable separately.

There are a couple of ways to fix that.

  1. Make sure you only have a single subscription. The answer by @Michael D exploits this approach. This is quite a common way to address the problem. A potential drawback is that the subscription manually created this way is not automatically unsubscribed from when the component is destroyed. In your example it might not be a big deal (if dayService$ only emits a single value). However, if the component is destroyed before the http request has finished, this http request would not be canceled without writing some extra code (involves implementing a ngOnDestroy lifecycle method). Another drawback - you would need to manually call changeDetector.markForCheck(), if your component uses OnPush.

  2. Make this observable "hot". "Hot" means the async action has initiated already, and all subscribers will just receive the result of that action - no matter how many subscribers there are. Using .toPromise(), as suggested by @xdeepakv, does exactly that. Note that promises are not cancellable at all - so you will have no way to cancel such a request. Another drawback - it only works if your observable emits a single value and then completes (e.g a single http request).

  3. You can use shareReplay({refCount: true}) operator to make your observable muticast - this allows multiple subscribers to share the same result. In this case you won't need to change your template, (can have multiple async pipes) and benefit from auto-unsubscribe / http request cancelation implemented in the async pipe.

this.webProtectionHTML$ = dayService$.pipe(
  mergeMap(...),
  map(...),
  shareReplay({refCount: true}) // <- making it a multicast
)
amakhrov
  • 3,820
  • 1
  • 13
  • 12
1

It is called three times because each async pipe fires a request. Instead in these cases you could subscribe in the component and use a member variable. You could then unsubscribe the subscriptions in the ngOnDestroy hook to avoid memory leaks. Try the following

Controller

private dayServiceSubscription: Subscription;
public webProtectionHTML: any;

ngOnInit() {
  this.dayServiceSubscription = this.dayService$
    .pipe(
      mergeMap((days: DaysPeriod) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
      map((html: string) => this.getWebProtectionData(html)))
    .subscribe(response => this.webProtectionHTML = response);
}

ngOnDestroy() {
  if (this.dayServiceSubscription) {
    this.dayServiceSubscription.unsubscribe();
  }
}  

Template

<div class="tr">
  <div class="align-left">Phishing & Other Frauds</div>
  <div class="align-right">{{ webProtectionHTML?.phishing}}</div>
</div>
<div class="tr">
  <div class="align-left">Spam URLs</div>
  <div class="align-right">{{ webProtectionHTML?.spamURLs}}</div>
</div>
<div class="tr">
  <div class="align-left">Malware Sites</div>
  <div class="align-right">{{ webProtectionHTML?.malware}}</div>
</div>
ruth
  • 29,535
  • 4
  • 30
  • 57
  • Thanks, I shoudn't use async pipe. – Di Wang Apr 04 '20 at 22:23
  • 1
    It is actually better in most cases because it handles the memory leak issues. But one thing to remember is each `async` pipe would subscribe individually to the observable. – ruth Apr 04 '20 at 22:29
  • Yes, async pipe should subscribles an Observable individually, just like a unique thread. I am a beginner on RXJS and Angular. Your timely response is very helpful. – Di Wang Apr 04 '20 at 22:37
1

For API, you can return as promise using toPromise method. It will convert subscribe to promise. So even you use async 3 times. It will resolve the promise once.

this.webProtectionHTML$ = this.dayService$
      .pipe(
        mergeMap((days: DaysPeriod // params added to request url) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
        map((html: string) => {
    //get html code and find data return as json data
    let result = this.getWebProtectionData(html)
          return result
        })).toPromise()
xdeepakv
  • 7,835
  • 2
  • 22
  • 32
1

It made 3 requests because the code specifies 3 async pipe for this.webProtectionHTML$ in the template.

You got two solutions:

1. Use single async pipe in the template

Use ngIf and set async pipe with alias as statement in the condition

<div *ngIf="webProtectionHTML$ | async as webProtection"> // define async here
    <div class="tr">
      <div class="align-left">Phishing & Other Frauds</div>
      <div class="align-right">{{ webProtection.phishing}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Spam URLs</div>
      <div class="align-right">{{ webProtection.spamURLs}}</div>
    </div>
    <div class="tr">
      <div class="align-left">Malware Sites</div>
      <div class="align-right">{{ webProtection.malware}}</div>
    </div>
</div>

2. Use shareReplay() rxjs operator The operator is used to replay the previous data to all the subscribers. shareReplay(1) means that replay the last data.

this.webProtectionHTML$ = this.dayService$
      .pipe(
        mergeMap((days: DaysPeriod // params added to request url) => this.httpClient.get(`api/data_processor_classlib.php....`//request url, { responseType: 'text' })),
        map((html: string) => {
          //get html code and find data return as json data
          let result = this.getWebProtectionData(html)
          return result
        })),
        shareReplay(1)

Both works but if I can choose for this case, I like the first solution.

Hope it helps

deerawan
  • 8,002
  • 5
  • 42
  • 51
  • Note that if `dayService$` can emit multiple values over time (generally, a never-ending observable), using shareReplay without refCount creates a memory leak – amakhrov Apr 05 '20 at 23:14
0

Because you called it 3 times by using (webProtectionHTML$|async), each time it called it and displayed the value of a different member. If you ant it to be called only once call the this.webProtectionHTML$ in the constructor or ngOnInit and assign the returned value to a local variable that you can use its value to bind on it.

Omar
  • 82
  • 5