1

I'm using the pokeapi to get the data of 151 pokemon. Since the API doesn't have a route to get all pokemon, I have to send 151 asynchronous get requests (one for each pokemon).

Despite the data being queried in order, I'm noticing the pokemon are returning in random order, with a different order each time. What are some strategies I can use to prevent this and keep them ordered in order of the requests (#1 - #151)? I'm assuming this is due to the asynchronous nature of the requests. Async/await could be useful but I haven't been able to implement it successfully here.

pokedex.component.html:

<div class="container">
    <div *ngFor="let pokemon of pokemons" class="pokemon-card">
        <div>
            <img src="{{ pokemon.sprites.front_default }}"/>
        </div>
        <span style="display:flex;">#{{pokemon.id}}</span>
        <span>{{pokemon.name | titlecase}}</span>
    </div>
</div>

pokedex.component.ts:

  constructor(public _httpService: HttpService) { }

  ngOnInit(): void {
     this.getPokemon();
  }

  pokemons = [];

  getPokemon() {
    this._httpService.getPokemon().subscribe( async (data) => {
      data.results.forEach((pokemon)=>{this.getPokemonDetails(pokemon.name)});
    });
  }

  getPokemonDetails(pokemon) {
    this._httpService.getPokemonDetails(pokemon).subscribe((data)=>{
      this.pokemons.push(data);
    });
  }

http.service.ts:

constructor(private http: HttpClient) { }

pokemonsUrl = 'https://pokeapi.co/api/v2/pokemon';

  getPokemon(): Observable<any> {
    return this.http.get(`${this.pokemonsUrl}?limit=151`);
  }

  getPokemonDetails(name): Observable<any> {
    return this.http.get(`${this.pokemonsUrl}/${name}`);
  }
Kyle Vassella
  • 2,296
  • 10
  • 32
  • 62
  • 2
    Try this: `this.pokemons[data.id - 1] = data;` (instead of pushing) –  Jan 31 '22 at 00:30
  • 1
    the above comment seems the easiest, just remember to handle the undefined case in ngFor. – ABOS Jan 31 '22 at 00:56
  • @ChrisG this appears to do the trick, although now I'm getting console errors like `ERROR TypeError: Cannot read properties of undefined (reading 'sprites')` even though the sprites load correctly. Why is it that `.push()` builds the array in random order, but using `array[data.id - 1]` keeps it in proper order? – Kyle Vassella Jan 31 '22 at 01:54
  • @ChrisG 's solution only works because your `id`s are sequential, unique, and start with 1. This is a fragile way to get the result you need, and shouldn't be taken as a general solution. You are better off finding a way to either query for a sorted result, or sort the response. – pilchard Jan 31 '22 at 02:17
  • 1
    The issue is located here: ``data.results.forEach((pokemon)=>{this.getPokemonDetails(pokemon.name)});``what you could do is to use of "for of" statement and await in it. But this would mean only one request per time. Or you remember the index and sent it to getPokemonDetails to get the correct index for your list. Nevertheless you way of coding is maybe not the best. Try to use only one subscription ...please check out switchMap, merge, combine ... etc. – Thomas Renger Jan 31 '22 at 08:15
  • 1
    You're running all requests in parallel, so they will finish in an arbitrary order. Which means you're pushing them into the array in an arbitrary order. When you use my line instead, each object is put at the correct index. This means the array will also get a bunch of `undefined` elements in between, until the respective requests finish. The proper solution is to use [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) –  Jan 31 '22 at 08:52

3 Answers3

1

You could skip the usage of async inside the subscription, and use RxJS higher order mapping operator like switchMap with forkJoin function to trigger multiple requests in parallel.

The order of output is ensured to be the order of input observables.

constructor(public _httpService: HttpService) { }

ngOnInit(): void {
  this.getPokemon();
}

pokemons = [];

getPokemon() {
  this._httpService.getPokemon().pipe(
    switchMap((data) => 
      forkJoin(
        data.results.map((pokemon: any) => 
          this._httpService.getPokemonDetails(pokemon)
        )
      )
    )
  ).subscribe({
    next: (results: any) => this.pokemons = results,
    error: (error: any) => {
      // handle error
    }
  })
}

Having said that, be aware of domain specific limitations to max number of parallel requests from browsers. If you feel the parallel requests slow down your application, refer here for an alternative.


Update

I created a working example for the code in Stackblitz and noticed there is an error in the getPokemonDetails() method.

At the moment you seem to be passing the whole object in the URL. It would result in an error. Also each object contains it's corresponding URL in the response. It would be wise to use it, so that any future changes to the API can't possibly affect your application.

I made the following changes:

  1. Since we are subscribing to the observable only to use the emissions in the template, you could replace the subscription in the controller (.ts) and use async pipe in the template (.html).

  2. Send the URL to the getPokemonDetails() to fetch the information.

Component controller (*.ts)

export class AppComponent {
  pokemons$: Observable<any>;

  constructor(public _httpService: HttpService) {}

  ngOnInit(): void {
    this.getPokemon();
  }

  getPokemon() {
    this.pokemons$ = this._httpService
      .getPokemon()
      .pipe(
        switchMap((data: any) =>
          forkJoin(
            data.results.map((pokemon: any) =>
              this._httpService.getPokemonDetails(pokemon.url)
            )
          )
        )
      );
  }
}

Component Template (*.html)

<div class="container">
  <div *ngFor="let pokemon of pokemons$ | async" class="pokemon-card">
    <div>
      <img src="{{ pokemon.sprites.front_default }}" />
    </div>
    <span style="display:flex;">#{{ pokemon.id }}</span>
    <span>{{ pokemon.name | titlecase }}</span>
  </div>
</div>

Service

getPokemonDetails(url): Observable<any> {
  return this.http.get(url);
}

Working example: Stackblitz

ruth
  • 29,535
  • 4
  • 30
  • 57
1

Since you mentioned async / await, I'll show you how to implement that as well. You can't await an observable on its own, but you can wrap the observable in a promise.

I would rework http.service.ts like this

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class HttpService {
  constructor(private http: HttpClient) {}

  pokemonsUrl = 'https://pokeapi.co/api/v2/pokemon';

  getPokemon(): Promise<any> {
    return new Promise((resolve) =>
      this.http
        .get(`${this.pokemonsUrl}?limit=151`)
        .subscribe((data: any) => resolve(data.results))
    );
  }

  getPokemonDetails(name: string): Promise<any> {
    return new Promise((resolve) =>
      this.http
        .get(`${this.pokemonsUrl}/${name}`)
        .subscribe((data) => resolve(data))
    );
  }
}

Then the component becomes much simpler

  constructor(public _httpService: HttpService) {}

  ngOnInit(): void {
    this.getPokemon();
  }

  pokemons: any[] = [];

  async getPokemon() {
    const pokemon: any[] = await this._httpService.getPokemon();
    for (const p of pokemon) {
      this.pokemons.push(await this._httpService.getPokemonDetails(p.name));
    }
  }

But this is a bit slow since we're making api calls one by one. To speed things up, you can put the promises in an array, and use Promise.all to await concurrently.

  async getPokemon() {
    const pokemon: any[] = await this._httpService.getPokemon();
    const promises = [];
    for (const p of pokemon) {
      promises.push(this._httpService.getPokemonDetails(p.name));
    }
    this.pokemons = await Promise.all(promises);
  }
Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
0

A simple solution would be to use the index parameter of forEach

  constructor(public _httpService: HttpService) { }

  ngOnInit(): void {
     this.getPokemon();
  }
  
  pokemons = [];

  getPokemon() {
    this._httpService.getPokemon().subscribe((data) => {
      data.results.forEach((pokemon, index)=>{this.getPokemonDetails(pokemon.name, index)});
    });
  }

  getPokemonDetails(pokemon, index) {
    this._httpService.getPokemonDetails(pokemon).subscribe((data)=>{
      this.pokemons[index] = data;
    });
  }

You just need to make sure you aren't trying to access an empty slot in the html

<div class="container">
  <ng-container *ngFor="let pokemon of pokemons">
    <div *ngIf="pokemon" class="pokemon-card">
      <div>
        <img src="{{ pokemon.sprites.front_default }}" />
      </div>
      <span style="display: flex">#{{ pokemon.id }}</span>
      <span>{{ pokemon.name }}</span>
    </div>
  </ng-container>
</div>

Works for me. I've also added an async / await solution as a separate answer, that one does not create empty slots in the array.

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26