0

I'm trying to make a React app to fetch data from the pokemon api (https://pokeapi.co/docs/v2#pokemon) and display it in a grid.

My component tree:

App -> Pokedex -> n(PokeCard -> PokeAvatar)

In the App component, I fetch the pokemon data, I get the 20 first results and map the resulting array to one where the URL of each Pokemon is also fetched (each Object inside of the Array has a 'name', 'url', and 'info' property). The info property holds all the data from each individual fetch.

After rendering the Pokedex with this array as props, in the Pokedex component I map the array to an array of elements containing only the data I want to display (name, and a few properties from the 'info' property).

Here's the code that raises the error:

export class Pokedex extends React.Component {
  render() {
    console.log("this.props.pokeArray:", this.props.pokeArray); // shows pokemons with the 3 properties
    const elements = this.props.pokeArray.map((pokemon, i) => {
      console.log(pokemon); // logs the pokemon without the info property
      return (
        <PokeCard
          key={`poke${i}`}
          id={pokemon.info.id} //error raised here: Cannot read property 'id' of undefined
          name={pokemon.name}
          types={pokemon.info.types}
          sprite={pokemon.info.sprites["front_default"]}
        />
      );
    });

    return (
      <div className="pkdx-pokedex-container">
        <h1>Pokedex</h1>
        {elements}
      </div>
    );
  }
}

Here's also the code from its parent element, App:

import "./App.css";
import { Pokedex } from "./components/Pokedex/Pokedex";
import { useQuery } from "react-query";
import { ReactQueryDevtools } from "react-query-devtools";

// *** Base API url

const url = "https://pokeapi.co/api/v2/pokemon";

// *** Async, because we need to have the data before second fetch

async function fetchPokemon() {
  const response = await fetch(url);

  const data = (await response.json()).results;

  // *** Keep url of fetch and add new info property to each pokemon

  data.forEach(async (poke) => {
    const res = await fetch(poke.url);
    poke.info = await res.json();
  });

  return data;
}

function App() {
  const info = useQuery("fetchPokemon", fetchPokemon);

  if (info.status === "success") {
    console.log("pokeArray:", info.data); // each Pokemon has the three properties
    return (
      <div>
        <Pokedex pokeArray={info.data} />;
        <ReactQueryDevtools />
      </div>
    );
  } else return null;
}

export default App;

I don't know if I'm missing something, but I don't understand why it doesn't show the 'info' property.

areberuto
  • 49
  • 1
  • 7

2 Answers2

1

It looks like you are doing an async forEach and not awaiting it. You may want to change to a map and do a const data = await Promise.all(data.map(...)) to ensure your data has loaded.

I put together a working example. Check it out:

import React from "react";
import { useQuery } from "react-query";
import { ReactQueryDevtools } from "react-query-devtools";

export class Pokedex extends React.Component {
  render() {
    console.log("this.props.pokeArray:", this.props.pokeArray); // shows pokemons with the 3 properties
    const elements = this.props.pokeArray.map((pokemon, i) => {
      console.log("POKEMON", pokemon); // logs the pokemon without the info property
      return (
        <React.Fragment key={i}>
          <div>key={`poke${i}`}</div>
          <div>id={pokemon.info.id}</div>
          <div>name={pokemon.name}</div>
          <div>sprite={pokemon.info.sprites["front_default"]}</div>
        </React.Fragment>
      );
    });

    return (
      <div className="pkdx-pokedex-container">
        <h1>Pokedex</h1>
        {elements}
      </div>
    );
  }
}

// *** Base API url

const url = "https://pokeapi.co/api/v2/pokemon";

// *** Async, because we need to have the data before second fetch

async function fetchPokemon() {
  const response = await fetch(url);

  const data = (await response.json()).results;

  // *** Keep url of fetch and add new info property to each pokemon

  const x = await Promise.all(
    data.map(async (poke) => {
      const res = await fetch(poke.url);
      return {
        ...poke,
        info: await res.json()
      };
    })
  );

  return x;
}

function App() {
  const info = useQuery("fetchPokemon", fetchPokemon);

  if (info.status === "success") {
    console.log("pokeArray:", info.data); // each Pokemon has the three properties
    return (
      <div>
        <Pokedex pokeArray={info.data} />;
        <ReactQueryDevtools />
      </div>
    );
  } else return null;
}

export default App;
jack.benson
  • 2,211
  • 2
  • 9
  • 17
  • hey! Thank you so much for the quick response. I've just tried your solution and it works, but I still don't get why: In the Pokedex component, the array from the props does have the info property set, so when feeding it to the map function it *should* display it, but inside the function the info property is lost. That's what seems weird :\ – areberuto Nov 10 '20 at 20:08
  • It is because the data has not finished loading yet. You have an `async` function in your `forEach` which is not awaited, hence the code continues before the values have been resolved. Consequently, you are trying to render with data that hasn't loaded yet. – jack.benson Nov 10 '20 at 20:43
  • hmmm, I will look it up more thoroughly. Thank you for taking the time! – areberuto Nov 14 '20 at 09:43
1

The problem here lies within fetchPokemon. When using await fetch(poke.url) within the forEach callback the callback will happily await the response. However forEach does not handle the promise returned by the callback. Meaning that the pokemon.info attribute is set some time after the data is returned from the fetchPokemon function.

To solve this use map() to store the resulting promises, then Promise.all() to await for a list of promises to resolve.

async function fetchPokemon() {
  const response = await fetch(url);
  const data = (await response.json()).results;

  await Promise.all(data.map(async (pokemon) => {
    const res = await fetch(pokemon.url);
    pokemon.info = await res.json();
  }));

  return data;
}

An async function always returns a promise, so passing a async function to map() will map the current array elements to promises. This array can then be passed to Promise.all() which will wait for all promises to finish. The promises will all resolve to undefined because there is no return value within the map() callbacks, however the data is stored within data so we can return that instead.

const url = "https://pokeapi.co/api/v2/pokemon";

async function fetchPokemon() {
  const response = await fetch(url);
  const data = (await response.json()).results;

  await Promise.all(data.map(async (pokemon) => {
    const res = await fetch(pokemon.url);
    pokemon.info = await res.json();
  }));

  return data;
}


fetchPokemon().then(pokemon => {
  document.body.textContent = JSON.stringify(pokemon);
});
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • hey! Thank you so much for the quick response. I've just tried your solution and it works, but I still don't get why: In the Pokedex component, the array from the props does have the info property set, so when feeding it to the map function it *should* display it, but inside the function the info property is lost. That's what seems weird :\ – areberuto Nov 10 '20 at 20:08
  • @areberuto `forEach` doesn't do anything with the promise returned by the async function passed as callback. Meaning that the callback does wait, but the `fetchPokemon` function does not. It will move on to the next statement which is `return data` before the `info` properties are set. – 3limin4t0r Nov 10 '20 at 21:47
  • hmmm, I will look it up more thoroughly. Thank you for taking the time! – areberuto Nov 14 '20 at 09:43
  • @areberuto See also [Using async/await with a forEach loop](https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop) – 3limin4t0r Nov 14 '20 at 10:28
  • 3limin4t0r, thank you for the post example! I can see what you mean there, and going through the MDN docs I've read that forEach expects a synchronous callback, so using an async callback will tend to cause problems. I still wonder why the props of my child component display the answer with the promises resolved, though. – areberuto Nov 15 '20 at 17:32