What is the most effective method for waiting for multiple requests to complete?

I am working on a component that requires fetching data from multiple endpoints through independent API calls. I want to make all these calls simultaneously and only load the user interface once all the data has been fetched successfully.

My approach involves using forkJoin in RxJS, but as someone new to this library, I'm not completely confident that I'm implementing it correctly. Below is an example of my code. I would appreciate any assistance or guidance.

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;
    });
  }
}

This implementation is inspired by a tutorial I came across while researching how to use forkJoin for this specific purpose. It meets my requirements perfectly as it allows me to process the results for each API call within their respective subscriptions and then display the data in the view using *ngIf or equivalent when all calls are complete.

However, an alternative approach that I frequently encounter in online tutorials is 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; 
    });

Although this approach seems complex and harder to interpret, I see it being used often. Am I missing something fundamental or is there a key concept I am overlooking?

In essence, I am seeking the most efficient Angular/RxJS solution for handling multiple API calls simultaneously and processing the results accordingly.

If I am on the right track, any feedback on potential improvements or corrections to my current implementation would be highly valuable. Thank you for any assistance in unraveling this for me.

Answer №1

In my opinion, the approach you take should be tailored to the specific task at hand.

If your objective is to wait for all Observables to complete and then manipulate their emitted values, you can use some convenient syntax:

forkJoin([obs1, obs2, obs3])
  // Utilizing array destructuring, a neat feature of EcmaScript 6.
  .subscribe(([val1, val2, val3]) => {
     this.val1 = val1;
     this.val2 = val2;
     this.val3 = val3
  }
);

Furthermore, you have the option to modify the observables before passing them to forkJoin, for example:

const modifiedObs1 = obs1
  .pipe(
     map(value => someTransformation(value))
     tap(modifiedValue => this.val1 = modifiedValue)
  );

forkJoin([modifiedObs1, obs2, obs3]).subscribe(() =>
  this.processingFinished = true;
)

Answer №2

Your code looks good. To improve maintainability and readability, it's recommended to use trailing $ for naming observables. Instead of subscribing and setting values individually, you can create an observable that maps the values in the desired object format for the template. This approach results in cleaner and more concise code.

For example:

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
      ))})));

You can then async pipe the data$ in the template, simplifying the process of subscribing and unsubscribing. Additionally, by strongly typing the return value of your mapped function, you can easily check it in the template or within the component as an Observable. I hope this explanation is helpful.

Using forkJoin appears to be the appropriate operator for this scenario :)

Answer №3

Seems like everything is in order, but here are a couple of suggestions:

It would be beneficial to create a service that handles the task of making http requests and returning their responses. This service would contain:

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

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

// Organizing api interactions in this manner is recommended.

When using forkJoin, I recommend separating the observable responses into their own variables like this:

forkJoin([this.myService.getMyEndpointData(), 
          this.myService.getMyEndpointDataById(1)
         ]).subscribe(([data, idData]: any) => {
// Perform actions using the received data
}

If you want to handle failures while allowing other observables to complete, ensure to catch errors within the inner observables of the forkjoin:

forkJoin([this.myService.getMyEndpointData().pipe(catchError(x => of(null))), // Handling errors to allow other endpoints to succeed, you can check for null in the subscription code.
          this.myService.getMyEndpointDataById(1)
         ]).subscribe(([data, idData]: any) => {
if(data) {
// Perform actions using the received data
}
}

If any observable within the forkJoin fails and is not handled, it may cause the entire operation to fail (possibly silently).

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Can the contents of a JSON file be uploaded using a file upload feature in Angular 6 and read without the need to communicate with an API?

Looking to upload a JSON file via file upload in Angular (using version 6) and read its contents directly within the app, without sending it to an API first. Have been searching for ways to achieve this without success, as most results are geared towards ...

Error message: The Dockerfile unexpectedly cannot find the Json-server command during execution

I am attempting to create a Dockerized Angular + Json-Server application, but I am encountering difficulty setting up json-server. Even though I have installed it in the Dockerfile, when using docker logs, it tells me that it couldn't find the command ...

Jasmine test case for Angular's for loop (Error: Expected the length of $ to be 11, but it should be 3

While creating test cases for a loop in Angular (Jasmine), I encountered an error: Error: Expected $.length = 11 to equal 3.. The loop is supposed to generate an array of arrays similar to const result. Can someone point out what I might have missed here? ...

Issue: Module "expose?Zone!zone.js" could not be located

Just started experimenting with Angular 2 and encountering an issue when importing zone.js as a global variable: https://i.stack.imgur.com/gUFGn.png List of my packages along with their versions: "dependencies": { "angular2": "2.0.0-beta.3", "es ...

"Obtaining subnet identification using the name or CIDR: A step-by-step

I am seeking help on retrieving the subnet id based on subnet name or cidr in order to deploy a nat gateway. Can someone provide guidance on how to obtain the subnet id? Alternatively, does anyone have any best practices for utilizing typescript function ...

Restrict the frequency of requests per minute using Supertest

We are utilizing supertest with Typescript to conduct API testing. For certain endpoints such as user registration and password modification, an email address is required for confirmation (containing user confirm token or reset password token). To facilit ...

The use of `super` in Typescript is not returning the expected value

Trying to retrieve the name from the extended class is causing an error: Property 'name' does not exist on type 'Employee'. class Person { #name:string; getName(){ return this.#name; } constructor(name:string){ ...

Setting the value of a form control within a form array in an Angular reactive form can be achieved by following these steps

I have a reactive form with multiple entity entries: this.entityDetailsForm = new FormGroup({ entitiesArray: new FormArray([ new FormGroup({ id: new FormControl(), name: new FormControl(), startDate: new Form ...

Is there a way to access the value of a public variable within the @input decorator of a function type?

I am working on a dropdown component that utilizes the @Input decorator to define a function with arguments, returning a boolean value. dropdown-abstract.component.ts @Input() public itemDisabled: (itemArgs: { dataItem: any; index: number }) => boo ...

What are some strategies for circumventing the need for two switches?

My LayerEditor class consists of two private methods: export class LayerEditor { public layerManager: LayerManager; constructor() { this.layerManager = new LayerManager(this); } private executeCommand() { ...

The function tokenNotExpired encounters an error when attempting to access the localStorage, as it

I am looking to incorporate the angular2-jwt library into my project: https://github.com/auth0/angular2-jwt However, I encountered an exception when attempting to call the tokenNotExpired function: Exception: Call to Node module failed with error: Refe ...

Tips for efficiently resolving and compiling a bug within an NPM package, ensuring it is accessible to the build server

This question may seem a bit unconventional. I am currently using an npm package that includes built-in type definitions for TypeScript. However, I have discovered a bug in these definitions that I am able to easily fix. My goal is to make this updated ve ...

Encountering a "Module parse failed" error with type annotations in Nextjs while using Yarn Workspaces

I decided to experiment with transitioning a project from using Vite and React to Next.js and React. After reviewing the documentation on this page: https://nextjs.org/learn-pages-router/foundations/from-react-to-nextjs/getting-started-with-nextjs I made t ...

Modify the [src] attribute of an image dynamically

I have a component that contains a list of records. export class HomeComponent implements OnInit { public wonders: WonderModel[] = []; constructor(private ms: ModelService){ ms.wonderService.getWonders(); this.wonders = ms.wonder ...

Describing an Object with some typed properties

Is there a method to specify only a portion of the object type, while allowing the rest to be of any type? The primary objective is to have support for intelliSense for the specified part, with the added bonus of type-checking support. To demonstrate, let& ...

Transferring data from the server to the client side in Next JS with the help of getInitialProps

I am currently developing an application using nextJS. Within server/index.ts, I have the following code: expressApp.get('/', (req: express.Request, res: express.Response) => { const parsedUrl = parse(req.url, true); const { query } = ...

encountering a loading issue with angular2-modal in rc1 version

Currently, I am implementing a library called angular2-modal. This library offers various modal options including bootstrap and vex for angular 2. While vex is functioning properly, I encounter an error when switching to bootstrap: responsive-applicati ...

Next.js React Server Components Problem - "ReactServerComponentsIssue"

Currently grappling with an issue while implementing React Server Components in my Next.js project. The specific error message I'm facing is as follows: Failed to compile ./src\app\components\projects\slider.js ReactServerComponent ...

Creating a component with the name "c-name" is not possible because the specified module does not exist

Current working directory /src/app Directory of app.module.ts /src/app/app.module.ts Adding a new component to this directory catalog/single/configurator/[new component here] Attempt #1 to add a component ng g c catalog/single/configurator/details-popo ...

After upgrading from angular version 7.0 to 8.0, I realized that the target in the tsconfig.json file remained unchanged

After running the command ng update @angular/cli @angular/core, I successfully updated my Angular 8 version. The updated versions are as follows: Angular CLI: 8.0.1 Node: 10.15.3 OS: linux x64 Angular: 8.0.0 ... animations, cdk, common, compiler, compiler ...