Search code examples
c#angulartypescriptrxjsobservable

Looking for assistance to leverage any RxJs methods when taming http calls for repeating Angular components


I have an Angular parent component that controls a list of objects, which are spacecrafts. The array of objects is loaded into the parent component, I will just call it list, and my child components, or cards, receive an instance of this object each to display its information. I am using an *NgFor and calling the component selector and passing in spacecraft via @Input.

Note: There are two different components that use this logic, one which displays our spacecraft in a "list view" and another that allows us to display a "grid view" - similar to a desktop folder. There is a UI switch to determine which *ngFor loop is executed - list, or grid. They use the same list of spacecraft from the parent component list, with small amounts of additional information being fetched in each child component as needed.

When my list of spacecraft are returned, they contain objects/arrays within them, one of those arrays being the spacecraft's Tasks[]. These Spacecraft are carefully loaded in the back end (C#) and each nested entity won't necessarily have all levels of .Include() to bring the FULL object back, as each Task will have a boatload of its own entities/collections that aren't needed for these components.

Within our Task entity we have a collection of Event. (So, Spacecrafts have a collection of Tasks which contain Events.) We do not have a separate back end model property of Events[] on Spacecraft. When we pull our Spacecraft query for this component, we do not include the Events to be nested within their Tasks for each Spacecraft. Doing so would really have an impact on speed and we haven't previously had a need to access an Spacecraft's events on their own. We have separate services to get an spacecraft's Tasks[], in which we then include their Events[].

Recently, we were given a task to display a COUNT of all Events existing between all Tasks of an spacecraft in our list component, separated into 3 categories of their Severity. We have a separate http method that allows us to grab all Event from the database whose property spacecraftId matches the id of a given spacecraft without having to go 3 levels deep in a LINQ query and return unneeded information and navigate through an spacecraft's Tasks. This works perfectly in every other area of the application.

What I am currently doing is loading my list of spacecraft into my list component, then making a separate call with a loop to obtain each spacecraft's Events[] and mapping each response to the spacecraft model's FRONT END only property of 'Events[]' (that I have added for this task).

It would make sense to load the Events[] data in the child components once it is received via Input(), however, doing the same call to obtain each spacecraft's Events[] when the view is flipped between the two view types is far less than ideal, because it's just a ton of data.

Ideally, I would like to have the spacecraft[] loaded in the parent component, so I can just re-use the same data and not have endless http calls. I hope that makes sense. My current code is below and it utilizes async/await, and it does work. I had to implement async/await, since the child components were loading before the Events[] were being returned to their spacecraft and the spacecraft were not updating once the http calls completed. (I also tried setting a boolean to flip when the last iteration of Events[] was complete - didn't work!)

List Component:

public spacecrafts: Spacecraft[] = [];

ngOnInit(): void {
  getSpacecraft()
}
getSpacecraft() {
  this.spacecraftService.getSpacecrafts(filters).subscribe(async res => {
      this.spacecrafts = res;
      await this.getSpacecraftEvents();
});
public async getSpacecraftEvents() {
    for (let sp of this.spacecrafts) {
      await this.spacecraftService.getEventsBySpacecraftAsync(sp.id).then(
        res => {
          sp.Events = [...res];
        }
      )
    };
    this.isLoading = false;
}

service method:

getEventsBySpacecraftAsync(spacecraftId: string): any {
    let params = {
      withDetails: false
    };
    return this.http.get<any>(`${this.endpoint}/${spacecraftId}/Events`, { params: params    }).toPromise();
  }

I don't necessarily love this, as the components controlling the view are required to wait for another component to synchronously resolve data on their behalf. I am really trying to leverage any RxJs functionality to extract those extra Event calls fully into the service layer before the data is even touched in the component, but I'm not sure where to go with it. Is there a better way to do this? (Even if not RxJs, of course). I'm not able to utilize a resolver - that's another route I tried.

And, yes, I am 100% considering adopting a new back end flow to just have the spacecraft's Events[] arrive in the initial http call but I'd really love to see if any other options exist besides async/await.


Solution

  • First of all, I suggest you not to mix promises and observables if not necessary. If you can it's better to stick to observables in angular. Therefore, I would remove async from getEventsBySpacecraftAsync and make it return observable:

        getEventsBySpacecraft(spacecraftId: string): any {
        let params = {
          withDetails: false
        };
        return this.http.get<any>(`${this.endpoint}/${spacecraftId}/Events`, { params: params    });
      }
    

    Now in getSpacecraftEvents you are actually doing many calls for events one by one. You await for each call in a for loop. Instead you could do those calls simultanousely, which should speed up this process a lot. Also since we switched to observables in service, we do it here as well:

    public getSpacecraftEvents() {
      forkJoin(
         this.spacecrafts.map(sp => this.spacecraftService.getEventsBySpacecraft(sp.id)))
        .subscribe(events => sp.Events = events);
      this.isLoading = false;
    }
    

    Instead of for loop we maped array of spacecrafts to array of observables, and joinded them together with forkJoin. After subscription, they were simultanousely requested.

    It's still not perfect, because it's not awaited after you fetch spacecrafts, instead it's done in separate subscribe. So lets fix it even more, we will make it a method that will return an observable, but not subscribe to it, and later we will use it in getSpacecraft().

    public getSpacecraftEvents() {
      return forkJoin(
        this.spacecrafts.map(sp => this.spacecraftService.getEventsBySpacecraft(sp.id)));
    }
    

    and getSpacecraft method:

    getSpacecraft() {
      this.spacecraftService.getSpacecrafts(filters)
        .pipe(switchMap(spacecrafts => {
          this.spacecrafts = spacecrafts;
          return this.getSpacecraftEvents()
        })).subscribe(events => {
          this.spacecrafts.foreach((sp, idx) => {
            this.spacecrafts[idx].events = events.filter(e => e.spacecraftId === sp.id);
          });
        });
    

    Now with switchMap we joined both series of calls in one observable that is executed in one subscribe. And event calls will execute simultanousely.