Best practices for nested async inside of ngFor?

522 Views Asked by At

Let’s say I have a list of cars:

const cars: Array<{carId: number}> = [{carId: 1}, {carId: 2}];

And I render them in a template:

<div *ngFor=“let car of cars”></div>

Inside of that template, I want to get some dynamic image paths from a method that returns an observable:

<div *ngFor=“let car of cars”>
    <img [src]=“getImagePath(car) | async”>
</div>

getImagePath(car: {carId: number}): Observable<string> {
   return //some service request that brings back a url based on the carId
}

For state management and storage, I’m using Firebase. All of my images are stored in a bucket and I can get to them by providing my service with a carId that uses it to fetch the downloadURL.

I want to avoid updating Firestore with downloadURL’s when the file is first saved because I’d be storing them in sub-collections and pulling them out of there is already a pain due to the nature of Firestore.

Is there a way to do this more efficiently? The code above would most certainly bring the browser to a crawl if the list starts to grow … right?

1

There are 1 best solutions below

1
On

Instead of calling a request directly on every img element, you can just make it into an observable and subscribe to it once then display the returned array to your html.

I used a couple of RXJS operators to get what you want.

  • mergeMap to flatten the inner observable
  • of to convert the cars array into an observable
  • map to transform the cars array into a a new array with their corresponding car image path
  • forkJoin - we wrapped multiple requests into one observable and will only return when a response has been received for all requests.

This is certainly only one way to do it and there might be other better ways.

getCarImages() {
  const cars: Array < {
    carId: number;
  } > = [{
      carId: 1,
    },
    {
      carId: 2,
    },
  ]; 
  of(cars)
  .pipe(
      mergeMap((e) => {
        return forkJoin(
          e.map((item) => this.apiService.getCarImages(item.carId))
        ).pipe(
          map((paths) => {
            console.log(paths);
            return e.map((car, index) => ({
              ...car,
              imagePath: paths[index],
            }));
          })
        );
      })
    )
    .subscribe((res) => {
      this.cars = res;
      console.log(this.cars);
    });
}

HTML:

<div *ngFor="let car of cars">
  <img [src]="car?.imagePath" />
</div>

I created a stackblitz to mock the solution.