1

Hi I'm trying to fetch a country's data after which I want to fetch the names for its neighboring countries.

import React from 'react';

export default function DetailsRoute({ match }) {

    const [countryData, setCountryData] = React.useState({});
    const [borderCountries, setBorderCountries] = React.useState([]);

    React.useEffect(() => {
        fetchCountryData();
    }, [])

    const fetchCountryData = async () => {
        /* fetch country by Name */
        const response = await fetch(`https://restcountries.eu/rest/v2/name/${match.params.country}`);
        const fetchedData = (await response.json())[0];

        setCountryData(fetchedData);

        const neighbors = [];

        /* Extract 'alphaCode' for each bordered country and fetch its real name */

        fetchedData.borders.forEach(async (alphaCode) =>  {

            const response = await fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`);
            const fetchedNeighbor = await response.json();

            neighbors.push(fetchedNeighbor.name);
        });

        /* THIS DOESN'T WAIT FOR NEIGHBORS TO BE FILLED UP */
        setBorderCountries(neighbors);
    }

    
    return (
        <article>
            <h1>{countryData.name}</h1>
            {borderCountries.map(countryName => <h2>{countryName}</h2>)}
        </article>
    )
}

As you can see, the setBorderCountries(neighbors) doesn't run asynchronously. But I have no idea how to make it wait for the forEach() loop to finish.

Somewhere on stackoverflow, I saw Promise.all() and tried to implement it but I really don't know if it's syntactically correct or not-

Promise.all(
    fetchedData.borders.forEach(async (alphaCode) => {

    const response = await fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`);
    const fetchedNeighbor = await response.json();

    neighbors.push(fetchedNeighbor.name);
    })
)
.then(() =>
    setBorderCountries(neighbors)
)

My question is how do I make the setBorderCountries(neighbors) wait until forEach() loop finishes filling up neighbors?

And maybe some suggested optimization to my code?

Sapinder
  • 183
  • 10
  • 1
    Please go through https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop, you may find some insights. – Asutosh Aug 22 '20 at 06:00

3 Answers3

2

The forEach loop finishes immediately, since the waiting happens in the callbacks (only), but those callbacks are all called synchronously, and so the forEach finishes before any of the awaited promises resolve.

Instead use a plain for loop:

for (let alphaCode of fetchedData.borders) {

Now the await inside that loop is part of the top level async function, and so it works as you intended.

You can also consider using Promise.all if it is acceptable that promises are created without waiting for the previous one to resolve. And then you can await the result of Promise.all. In your attempt at this, you did not pass anything to Promise.all, as forEach always returns undefined. The correct way would use .map as follows:

const neighbors = await Promise.all(
    fetchedData.borders.map(async (alphaCode) => {
        const response = await fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`);
        const fetchedNeighbor = await response.json();
        return fetchedNeighbor.name; // return it!!
    });
)
setBorderCountries(neighbors);

Note that here also the .map iteration finishes synchronously, but it returns an array of promises, which is exactly what Promise.all needs. The awaiting happens with the await that precedes Promise.all.

trincot
  • 211,288
  • 25
  • 175
  • 211
  • could you please elaborate *"those callbacks are called synchronously"*? – Sapinder Aug 22 '20 at 06:14
  • 1
    When a callback is called, and it encounters an `await`, it *returns* at that moment, returning a promise. All other callbacks are called immediately following that, and the same happens with them. And code execution continues with the statements after the `forEach` loop. Only when the awaited promises start to resolve, will execution go back to those callbacks to continue their execution, but then the `forEach` has long finished. – trincot Aug 22 '20 at 06:37
1

Promise.all takes an array of promises. Your code is passing the result of the forEach call (undefined) to Promise.all, which isn't going to do what you want.

If you use map instead you can create an array of request promises. Something along the lines of:

const fetchNeighbor = async (alphaCode) => {
  return fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`)
    .then(response => response.json())
    .then(n => n.name);
}

const neighborNames = await Promise.all(fetchedData.borders.map(fetchNeighbor));
setBorderCountries(neighbors);

Array.map produces a new array by running the given function on every element in the source array. So these two are roughly equivalent:

const promises = fetchedData.borders.map(fetchNeighbor);
const promises = [];
fetchedData.borders.forEach(alphaCode => {
  promises.push(fetchNeighbor(alphaCode));
});

You now have an array of promises (because that's what fetchNeighbor returns) you can pass to Promise.all:

const results = await Promise.all(promises);

Promise.all resolves with an array of the resolved promise values. Since fetchNeighbor ultimately resolves with the name, you now have an array of names.

const results = await Promise.all(promises);

console.log(results);
// ['Country A', 'Country B', 'Country C', ... ]
ray hatfield
  • 16,302
  • 4
  • 25
  • 21
  • So from what I understood- `.map()` takes each `borders` member and runs `fetchNeighbor`, which returns the corresponding `.name`. Maybe I still misunderstand the working of `Promise.all`, but **it collects the return value of each promise** (strings in this case) and puts them in an array. Am I right? – Sapinder Aug 22 '20 at 06:11
  • Yes. [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) returns a new array containing the result of running the function for each element in the original array. So in this case it produces an array of fetch promises from your array of borders. Promise.all resolves with an array of results when all of those promises resolve. I chained the `then()`s to get the json and return only the name, which seemed to be what you were after. – ray hatfield Aug 22 '20 at 06:16
  • Oh so you mean the collection task for return values of promises is performed by `.map()` and `Promise.all` just sits there for promises to be resolved, and takes the array ***returned*** by `.map()`. – Sapinder Aug 22 '20 at 06:20
0

I think you should use something like this.

const start = async () => {
  await asyncForEach([1, 2, 3], async (num) => {
    await waitFor(50);
    console.log(num);
  });
  console.log('Done');
}
start();

this article I think is a good resource to learn: async/await with for each