DarthSaul
DarthSaul

Reputation: 61

Resolve a promise / perform an asynchronous operation inside of .map()

I've been learning React and having an absolute blast. To practice using hooks and the Context API, I've created a very plain Pokemon application that fetches data from the (very awesome) PokéAPI. The problem I can't seem to solve is how to initialize my context by fetching data from a url that is retrieved from the first fetch for data. This is of course a terrible explanation but my code below clarifies.

I've wrapped an <App /> component in a <ContextProvider /> like so:

import React from 'react'
import ReactDOM from 'react-dom'

import App from './App';
import { ContextProvider } from './Context'

ReactDOM.render(
    <ContextProvider>
        <App />    
    </ContextProvider>, 
    document.getElementById('root')
);

Then jumping right over to my Context.js file, I'm fetching the data I need when the application first loads:

import React, { useState, useEffect } from 'react'
import fetchSprite from './utils/fetchSprite'
const Context = React.createContext()

function ContextProvider({ children }) {
    const [ allPokemon, setPokemon ] = useState([])
    console.log(allPokemon)

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch("https://pokeapi.co/api/v2/pokemon?limit=25")
            const pokemon = await response.json()
            
            /* Do ANOTHER async operation here */

            ))
            setPokemon(pokemon.results)
        }
        fetchData()
    }, [])

    return (
        <Context.Provider value={{ allPokemon }}>
            {children}
        </Context.Provider>
    )
}

export { ContextProvider, Context }

After parsing the JSON provided as a response, pokemon.results is an array of objects, where each object contains a name and url property. I'm trying to make an additional fetch call with the url on from each object so I can also store a string containing the link to a PNG image. Again, showing this in the code will make more sense:

import React, { useState, useEffect } from 'react'
import fetchSprite from './utils/fetchSprite'
const Context = React.createContext()

function ContextProvider({ children }) {
    const [ allPokemon, setPokemon ] = useState([])

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch("https://pokeapi.co/api/v2/pokemon?limit=25")
            const pokemon = await response.json()
            const pokeData = pokemon.results.map(obj => (
                {
                    ...obj,
                    sprite: fetchSprite(obj.url)
                }
            ))
            setPokemon(pokeData)
        }
        fetchData()
    }, [])

    return (
        <Context.Provider value={{allPokemon}}>
            {children}
        </Context.Provider>
    )
}

export { ContextProvider, Context }

Above, I'm taking the same array I would pass to setPokemon, and essentially copying the array but each object in the array has an additional property sprite.

My fetchSprite.js file looks like this:

async function fetchSprite(url) {
    const res = await fetch(url)
    const data = await res.json()
    const result = data.sprites.front_default
    return result
}

export default fetchSprite

I can't figure out how to resolve the Promise being returned by my fetchSprite function inside of my call to .map(). When the array is logged to the console, I have a fulfilled Promise, but can't figure out how to access the result:

Screenshot of array with fulfilled promises, logged to the console

How can I essentially have my .map() function wait for each promise to resolve before returning the object that's added to the array? I've tried all types of async combinations, like making the anonymous function inside of .map() an async function and use await for the sprite property. The resulting problem in the UI is the img tags I have reading the PNG url are broken. This has been killing me -- any and all help is very much appreciated!!!

Upvotes: 2

Views: 2531

Answers (1)

DarthSaul
DarthSaul

Reputation: 61

Thanks to see sharper for pointing me to Using async/await with a forEach loop!

The entire map operation should be wrapped in await Promise.all(), and the anonymous function inside of map should be an async function:

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch("https://pokeapi.co/api/v2/pokemon?limit=25")
            const pokemon = await response.json()
            const pokeData = await Promise.all(pokemon.results.map(async (obj) => {
                return {
                    ...obj,
                    sprite: await fetchSprite(obj.url)
                }
            }))
            setPokemon(pokeData)
        }
        fetchData()
    }, [])

Upvotes: 3

Related Questions