PengProgrammer
PengProgrammer

Reputation: 174

Promise only resolves correctly on page refresh

I am playing around with an API that gets a list of Pokemon and corresponding data that looks like this.

export function SomePage() {
const [arr, setArray] = useState([]);

   useEffect(() => {
    fetchSomePokemon();
  }, []);


  function fetchSomePokemon() {
    fetch('https://pokeapi.co/api/v2/pokemon?limit=5')
     .then(response => response.json())
     .then((pokemonList) => {
       const someArray = [];
     pokemonList.results.map(async (pokemon: { url: string; }) => {
       someArray.push(await fetchData(pokemon))
     })
     setArray([...arr, someArray]);
    })
   }

   async function fetchData(pokemon: { url: string; }) {
    let url = pokemon.url
     return await fetch(url).then(async res => await res.json())
    }

    console.log(arr);

  return (
      <div>
      {arr[0]?.map((pokemon, index) => (
          <div
            key={index}
          >
            {pokemon.name}
          </div>
        ))
      }
      </div>
  );
}

The code works(kind of) however on the first render the map will display nothing even though the console.log outputs data. Only once the page has been refreshed will the correct data display. I have a feeling it's something to do with not handling promises correctly. Perhaps someone could help me out. TIA

Expected output: Data populated on initial render(in this case, pokemon names will display)

Upvotes: 2

Views: 996

Answers (4)

kapnobatai137
kapnobatai137

Reputation: 81

One way to solve your issues is to await the data fetching in useEffect().

Here's a POC:

export function Page() {
  const [pokemon, setPokemon] = useState([]);

  // will fetch the pokemon on the first render
  useEffect(() => {
    async function fetchPokemon() {
      // ... logic that fetches the pokemon
    }
    
    fetchPokemon();
  }, []);

  if (!pokemon.length) {
    // you can return a spinner here
    return null;
  }
  
  return (
    <div>
      {pokemon.map(item => {
        // return an element
      })}
    </div>
  );
 }

Upvotes: 0

vatz88
vatz88

Reputation: 2452

The in-build map method on arrays in synchronous in nature. In fetchSomePokemon you need to return a promise from map callback function since you're writing async code in it.

Now items in array returned by pokemonList.results.map are promises. You need to use Promise.all on pokemonList.results.map and await it.

await Promise.all(pokemonList.results.map(async (pokemon: { url: string; }) => {
       return fetchData.then(someArray.push(pokemon))
     }));

Upvotes: 3

Glauco Heitor
Glauco Heitor

Reputation: 66

On your first render, you don't have the data yet, so arr[0] doens't exist for you to .map on it, so it crashes. You need to check if the data is already there before mapping.

Using optional chaining, if there's no data it will not throw an error on your first render and it will render correctly when the data arrive and it re-renders.

...
  return (
    <div>
      {arr[0]?.map((pokemon, index) => (
        <div key={index}>{pokemon.name}</div>
      ))}
    </div>
  );
}

Upvotes: 1

Madhu Kumar
Madhu Kumar

Reputation: 405

in

useEffect(() => { fetchSomePokemon(); }, []);

[] tells react there is no dependencies for this effect to happen,

read more here https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Upvotes: 0

Related Questions