0
votes

I wrote a function that gets an observable list of pokemon from the pokeapi.co API. If I want to use this function (getPokemons()), I need to subscribe to it. However the subscribe response returns an empty list, because in my function I do another API call using the subscribe function.

The internet clarified that subscribing within a http request is a common mistake, and that you need to use forkMap, flatMap, switchMap, mergeMap or whatever. Unfortunately, I do not understand how I can apply one of those functions to my case.

getPokemons(pageNumber: number) : Observable<Pokemon[]> {
    var apiUrl = `${this.apiRoot}?&limit=${this.itemCount}&offset=${pageNumber * this.itemCount - (20)}`;
    return this.httpClient.get<any>(`${apiUrl}`).pipe(
      map(res => {
        var results: Pokemon[] = [];
        for (let pokemon of res.results) {
          this.getPokemonByName(pokemon.name).subscribe(data => {
            results.push(data);
          },
          err =>  {
            console.error(err.error || 'API error');
          });
        }
        return results;
      })
    );
  }

getPokemonByName(name: string) : Observable<Pokemon> {
    return this.httpClient.get<Pokemon>(`${this.apiRoot}/${name}`).pipe(
      map(res => {
        return new Pokemon(res);
      })
    );
  }

In my scenario the original http.get() responds with a list that only has the name of the pokemon. I need to do another API call to get the details of a specific pokemon. Because getPokemonByName().subscribe() is async, the array 'results' stays empty.

I hope you understand my issue, and you can help me figure out how to work this out.

2

2 Answers

1
votes

you can make use of forkJoin like this -

getPokemons(pageNumber: number) : Observable<Pokemon[]> {
    var apiUrl = `${this.apiRoot}?&limit=${this.itemCount}&offset=${pageNumber * this.itemCount - (20)}`;
    return this.httpClient.get<any>(`${apiUrl}`)
               .pipe(
                      switchMap(res => {

                        //now you want to call getPokemonByName for each of the result your got in res.results
                        //so first create an array from getPokemonByName -
                        const observables$ = res.result.map(pokemon => this.getPokemonByName(pokemon.name));
                        return forkJoin(observables$);                        
                      })
                    );
  }

  getPokemonByName(name: string) : Observable<Pokemon> {
    return this.httpClient.get<Pokemon>(`${this.apiRoot}/${name}`).pipe(
      map(res => {
        return new Pokemon(res);
      })
    );
  }
}

Now your consumer of getPokemons should subscribe to the observable returned by getPokemons.

1
votes

You can indeed simplify your getPokemons() method with RxJS operators, such as forkJoin. forkJoin() will for the for..of loop to be completed before returning all the observables. If you are familiar with the usage of Promises in JavaScript, it is actually similar to Promise.all.

Once you have subscribed to it, you will be able to get the return values. And do not forget to import your required operators into your component!

import { forkJoin } from 'rxjs';
.
.
getPokemons(pageNumber: number): Observable < Pokemon[] > {
  const apiUrl = `${this.apiRoot}?&limit=${this.itemCount}&offset=${pageNumber * this.itemCount - (20)}`;
  const observablesList = [];

  return this.httpClient.get<any>(`${apiUrl}`).pipe(
    map(res => {
      const observablesList = [];
      for (let pokemon of res.results) {
        observablesList.push(this.getPokemonByName(pokemon.name));
      }
      return forkJoin(observablesList);
    })
  ).subscribe(res => {
    console.log(res);
    // do the rest here
  })
}