简体   繁体   中英

What is the correct way to wait for multiple requests to finish?

I have a component that requires multiple get requests to return all the data I need. These are independent of each other but I would like to make all the calls together and load the UI after this has completed successfully.

To accomplish this I'm using forkJoin but I'm new to RxJS and I'm not entirely positive I'm doing this correctly. Please see my example below. Any help would be greatly appreciated.

import { Component, Input, OnInit, ElementRef, ViewChild } from '@angular/core';
import { Observable, forkJoin, of } from 'rxjs';
import { flatMap } from 'rxjs/operators';
import { DataService, ItemGroupDto} from 'src/app/shared/services/data-service.service';
import { UserService} from 'src/app/shared/services/user-service.service';

@Component({
  selector: 'app-sample',
  templateUrl: './sample.component.html',
  styleUrls: ['./sample.component.less']
})

export class SampleComponent implements OnInit {

  @Input() itemGroupId: number;
  public datasetOne: Observable<any[]>;
  public datasetTwo: Observable<any[]>;
  public currentApprover: string;

  constructor(
    private _dateService: DateService,
    private _userService: UserService,
  ) {}

  ngOnInit() {
    this.getData();
  }

  private _getApprover(itemGroupId: number) : Observable<ItemGroupDto> {
    const approver = this._userService.getApprover(itemGroupId);
    approver.subscribe(results => {
      const approver = results.approvers.find(approver => (
        approver.Id === 1
      ));
      this.currentApprover = approver.UserFullName;    
    })
    return approver;
  }

  private _sampleDatasetOne() : Observable<any>{
    const sampleDatasetOne = this._dataService.sampleDatasetOne();
    sampleDatasetOne.subscribe(results => {
      this.datasetOne = results;
    })
    return sampleDatasetOne;
  }

  private _sampleDatasetTwo() : Observable<any>{
    const sampleDatasetTwo = this._dataService.sampleDatasetTwo();
    sampleDatasetTwo.subscribe(results => {
      this.datasetTwo = results;
    })
    return sampleDatasetTwo;
  }


  getData() {
    let sampleDatasetOne = this._sampleDatasetTwo();
    let sampleDatasetTwo = this._sampleDatasetTwo();
    let approver = this._getApprover(this.itemGroupId);
    
    forkJoin(sampleDatasetOne, sampleDatasetTwo, approver).subscribe(_ => {
      // all observables have been completed
      this.loaded = true;
    });
  }
}

The above code is based on an example I found while looking into using forkJoin for this purpose. This works great for my requirements. It allows me to manipulate the results for each within their respective subscriptions and then when all the observables are complete I can, for example, show the data in the view via *ngIf or equivalent.

However, I've also seen examples like the following:

let sampleDatasetOne = this._dataService.sampleDatasetOne();
let sampleDatasetTwo = this._dataService.sampleDatasetTwo();
let approver = this._userService.getApprover(this.itemGroupId);

forkJoin([sampleDatasetOne, sampleDatasetTwo, approver]).subscribe(results => {
      this.datasetOne = results[0];
      this.datasetTwo = results[1];
      const approver = results[2].approvers.find(approver => (
        approver.Id === 1
      ));
      this.currentApprover = approver.UserFullName; 
    });

I actually see this quite a lot in examples or tutorials online explaining this particular use of forkJoin. This seems kind of crazy, hard to read, maintain etc.. But maybe I'm just missing some key information or concept?

Essentially, I'm just looking for the right angular/rxJS way of waiting for multiple calls to complete and handling/manipulating the results for each.

If I'm on the right track, is there anything I'm missing, doing wrong or could improve upon?

Thanks to anyone for taking the time to answer and help me wrap my head around this.

Everything seems ok with a few pointers:

You're going to want to do is create a service that's sole responsible is to make http requests and returning their responses. Essentially a service full of :

  getMyEndpointData(): Observable<any> {
        return this.http.get<any>('foo/bar/etc');
  }

  getMyEndpointDataById(id): Observable<any> {
        return this.http.get<any>(`foo/bar/etc/${id}`);
  }

// this is a good way to organize api interactions.

Then for your forkJoin, I prefer the syntax that separates the observable responses into their own variables:

forkJoin([this.myService.getMyEndpointData(), 
          this.myService.getMyEndpointDataById(1)
         ]).subscribe(([data, idData]: any) => {
// do stuff with data recieved
}

If you want to accept failures and still have others complete:

forkJoin([this.myService.getMyEndpointData().pipe(catchError(x => of(null))), // this deals with errors and allows other endpoints to succeed, you can do a null check in subscription code.
          this.myService.getMyEndpointDataById(1)
         ]).subscribe(([data, idData]: any) => {
if(data) {
// do stuff with data recieved
}
}

The code you provided seems fine. For maintainability and readability, it's usually best to follow the trailing $ to name observables. Also, rather than subscribing and setting values, you can just maintain an observable that has the values mapped in the object format you want to bind to the template. This will make it less code and cleaner.

In this case, something like this:

let sampleDatasetOne$ = this._dataService.sampleDatasetOne();
let sampleDatasetTwo$ = this._dataService.sampleDatasetTwo();
let approver$ = this._userService.getApprover(this.itemGroupId);

let data$ = forkJoin([sampleDatasetOne$, sampleDatasetTwo$, approver$]).pipe(map(results => ({
      dataSetOne: results[0],
      dataSetTwo: results[1],
      approver: results[2].approvers.find(approver => (
        approver.Id === 1
      ))})));

the data$ can be async piped in the template (which will subscribe and unsubscribe). Additionally, your mapped function can strongly type the return value so it can be checked in the template or elsewhere in the component as an Observable. Hope this was helpful.

But the forkJoin seems like the proper operator to use in this case :)

I would say it depends on what you are doing.

If you just want all Observables to complete and then do something with their emitted values, some syntactic sugar helps:

forkJoin([observable1, observable2, observable3])
  // Array destructuring, a cool EcmaScript 6 feature.
  .subscribe(([value1, value2, value3]) => {
     this.value1 = value1;
     this.value2 = value2;
     this.value3 = value3
  }
);

This still leaves the possibility open, to process the observables before passing them to forkJoin, eg:

const mappedObservable1 = observable1
  .pipe(
     map(value1 => mapToSomething(value1))
     tap(mappedValue1 => this.value1 = mappedValue1)
  );

forkJoin([mappedObservable1, observable2, observable3]).subscribe(() =>
  this.loadingDone = true;
)

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