Home > Software engineering >  Waiting for multiple RxJS call before executing the next code block
Waiting for multiple RxJS call before executing the next code block

Time:12-22

I hava a list of streets and iterate over them. Inside the for loop i gonna call an endpoint for each of these streets. The endpoint gives me informations about the requested street. I wanna store each response into an Object Array and after ALL requests finished, i wanna execute the next code block. Here is my problem: I do all the calls, store all my data into the Object Array, but if i gonna use in the next code block my prefilled Object Array, the length = 0... Here my code:

export class MyComponent{
  addressInfoArray: AddressInfo[] = [];

  // some code ...

  prepareStreetInformations(): void {
    // some code ....

    this.fillArray(streets, url);
    this.doSomethingWithArray(this.addressInfoArray); // <--- length = 0 and doesn't waits for finishing the fillArray() method
  }
}

fillArray(streets: Street[], url: string): void { // streets has a length of 150
  for (const street of streets) {
    this.http.get<AddressInfo>(`${url}/street.name`).subscribe(response => {
      this.addressInfoArray.push(response);
    });
  }
}

So my question is: How can wait the doSomethingWithArray() method for the completely finishing of the fillArray() method and why is the doSomethingWithArray() method can't see that my Object Array is allready filled?

CodePudding user response:

Try using forkJoin ;

const streetsObservs = this.streets.map(street => this.http.get<AddressInfo>(`${url}/street.name`));
forkJoin(streetsObservs).subscribe((response: any) => {
  this.addressInfoArray = response; // this is an array
  this.doSomethingWithArray(this.addressInfoArray); // <--- length = 0 and doesn't waits for finishing the fillArray() method

});

CodePudding user response:

I think you have a wrong idea of how Observables work. In your sample code, executing fillArray just dispatches the http calls, afterwards the function ends. The next step in your code is the call to doSomethingWithArray and of course at this point addressInfoArray is not populated. Then, some time later when a http call finishes, the callback function you provide in subscribe is executed, calling addressInfoArray.push. This is always the case when using subscribe. It will just execute the function you pass to it when the Observable completes.

If you want to wait for many Observables to complete, you can use combineLatest. This creates an Observable that emits an array containing all the values from the original Observables. This is how it works:

prepareStreetInformations(): void {
  this.fillArray(streets, url).subscribe(addressInfoArray => {
    this.doSomethingWithArray(this.addressInfoArray)
  });
}
  
fillArray(streets: Street[], url: string): Observable<AddressInfo[]> {
  return combineLatest(
    streets.map(street => this.http.get<AddressInfo>(`url/${street.name}`))
  );
}

Note that fillArray now returns an Observable that you can subscribe to and is no longer writing any changes to a class property. If you need the class property, you have to define it inside the callback function passed to subscribe.

CodePudding user response:

With RxJS, you don't really try to force a function call to "wait" for a previous function; rather, you can construct an observable that emits the data you need based on other observables.

In your case, it looks like you need an observable that emits the array of AddressInfo.

We can define a getStreets() method that returns Observable<Street[]> and also a getAddressInfo() method that takes a street param and returns Observable<AddressInfo>.

Now, we can create an observable that will emit your AddressInfo[], using switchMap and forkJoin:

1  export class MyComponent {
2  
3    getStreets(): Observable<Street[]> {
4      this.http.get<Street[]>(...);
5    }
6  
7    getAddressInfo(street: Street): Observable<AddressInfo> {
8      this.http.get<AddressInfo>(`${this.url}/${street.name}`); // don't subscribe here
9    }
10
11   addressInfos$: Observable<AddressInfo[]> = this.getStreets().pipe(
12     switchMap(streets => forkJoin(
13       streets.map(s => this.getAddressInfo(s))
14     ))
15   );
16 
17 }

We use forkJoin to create a single observable that emits an array of the results of all its input observables. So, we pass it an array of observables as input.

Since you have an array of streets. On line #13, we simply map it to an array of observables that get the address info. Now, when the forkJoined observable is subscribed to, it will emit an array of AddressInfo (the results of all the individual http call)

We use switchMap to handle subscribing to this "forkjoined observable" for us. The result is a single addressInfos$ observable that emits the AddressInfo[].

Notice we have not subscribed yet. To do your work, you could simply subscribe:

addressInfos$.subscibe(
  infos => doSomethingWithArray(infos)
);

However, in angular, a typical way would be to further transform the emission of your data into the shape your template needs:

templateData$ = this.addressInfos$.pipe(
  map(infos => {
    //  do something will array :-)
  })
);

Then, in your template, you can leverage the async pipe:

<div *ngIf="templateData$ | async as data">
  <ul>
    <li *ngFor="item of data">{{ item }}</li>
  </ul>
</div>
  • Related