简体   繁体   中英

Angular Observable need to complete first

@Input() list: string[];

ngOnInit() : void {
   this.valueMap = new Map<any, any>();
   this.getDataFromService();
   this.buildContainer();
}

private getDataFromService(): void {
  this.list.forEach(value-> {
      this.fetchService(value).subscribe(data ->{
          this.valueMap.set(value,data);
          }
       )
   })
}

private buildContainer(): void {
   console.log(this.valueMap.size); // shows always 0 even when service returns the data 
 }

Now thing is that I have to use this valueMap in the method buidlContainer() , hence I need to get the complete map first from service. And when I use that map in buildConatainer() it shows me undefined.

I understood that this is some async call issue. Also, I can not call method buildContainer() for each value in.subscribe() as that would not be better idea. So, I have to compute the Map first before processing that further..

Any help is appreciated and i can not modify the service from returning Observable to return Promise

i want to do as below

private buildContainer(): void {
   for([key,data] of this.valueMap) {
      -----some code----
    }
 }

Simply use forkJoin to wait for all observables to finish. forkJoin will bundle all observables into one. This observable can be returned and inside ngOnInit you subscribe to it and call buildContainer .

 @Input() list: string[];

  ngOnInit(): void {
    this.valueMap = new Map<any, any>();
    this.getDataFromService().subscribe(_ => this.buildContainer());
  }

  private getDataFromService(): Observable<any> {
    return forkJoin(
      this.list.map(value =>
        this.fetchService(value).pipe(
          tap(data => this.valueMap.set(value, data))
        )
      )
    );
  }

  private buildContainer(): void {
    console.log(this.valueMap.size);
  }

The code would be even cleaner if you do not populate the valueMap inside the tap operator and instead use the result you get into your subscription function.

This is one of the disadvantage (or advantage depending how you look at it) of introducing reactive paradigm to your code. A single point of reactive procedure would gradually creep into other parts of the code either partially or completely.

So I'd recommend you to make the valueMap reactive as well and make it respond based on the changes in getDataFromService() service. One way would to make the valueMap variable a RxJS ReplaySubject with a buffer of 1. So it'll buffer (or hold) it's last value pushed to it and emit it immediately upon subscription.

Try the following

import { forkJoin, ReplaySubject, Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';

valueMap: ReplaySubject<Map<any, any>> = ReplaySubject<Map<any, any>>(1);
completed$ = new Subject<any>();

@Input() list: string[];

ngOnInit(): void {
  this.valueMap = new Map <any, any>();
  this.getDataFromService();
  this.buildContainer();
}

private getDataFromService(): void {
  forkJoin(this.list.map(value => this.fetchService(value))).pipe(
    takeUntil(this.completed$)             // <-- close subscription upon component `ngOnDestroy` hook trigger
  ).subscribe(
    data => {
      let map = new Map<any, any>();
      data.forEach((item, i) => {
        map.set(this.list[i], item);
      });
      this.valueMap.next(map);
    }
  );
}

private buildContainer(): void {
  this.valueMap.asObservable().pipe(
    take(1)               // <-- emit only the first notification and complete
  ).subscribe(
    data => {
      console.log(this.valueMap.size);
    }
  );
}

ngOnDestroy() {
  this.completed$.next();
  this.completed$.complete();
}

I've also used RxJS forkJoin() function to combine multiple observables and takeUntil operator close the subscription when the component is destroyed.

Working example: Stackblitz

In the example, I've used jsonplaceholder API to mock some HTTP calls in the fetchService() function.

Note:

  1. The forkJoin() will emit only if all the observables complete. If you're dealing with a stream of data, you will have to replace it with either combineLatest or zip based on your requirement.

  2. There is an obvious issue where if the buildContainer() function is called multiple times in quick successions, each will trigger individual subscriptions.

Remove buildContainer from ngOnInit and Call the method inside the subscribe

 private getDataFromService(): void {
    this.list.forEach((value, index) -> {
        this.fetchService(value).subscribe(data ->{
            this.valueMap.set(value,data);
                   }
         )
         if(this.list.length-1 == index){
          this.buildContainer(); // call here
         }
     })
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM