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?
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.
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', ... ]
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
Related
I am mapping through this dummyAssests array and want to create a new array of objects based on the axios response called newAssetArray. But when I console.log the newAssetArray it just contains an array of Promises...
useEffect(() => {
let newAssetArray = dummyAssets.map(async function (asset) {
return await axios
.get(currrentPrice(asset.asset_id))
.then((response) => {
let cp = response.data.market_data.current_price.eur;
let value = Number(cp) * Number(asset.amount);
return { ...asset, value: +value };
})
.catch((error) => console.log(error.response.data.error));
});
setAssets(newAssetArray);
console.log(newAssetArray);
}, [dummyAssets]);
Console:
Yes, that is expected. Use Promise.all() to wait for all the promises to resolve. .map() itself is not promise aware so it does not "wait" for each individual iteration of its loop to resolve before going onto the next one. So, you have the result of calling N async functions which all return a promise as your .map() result. That is how the code is supposed to work. The await you are using it not accomplishing anything. If you use a for loop instead (which is promise-aware, then your loop will be sequenced.
Here's how you could use Promise.all() to get the results:
useEffect(() => {
Promise.all(dummyAssets.map(function(asset) {
return axios
.get(currrentPrice(asset.asset_id))
.then((response) => {
let cp = response.data.market_data.current_price.eur;
let value = Number(cp) * Number(asset.amount);
return { ...asset, value: +value };
}).catch((error) => {
console.log(error.response.data.error)
throw error;
});
})).then(newAssetArray => {
console.log(newAssetArray);
setAssets(newAssetArray);
}).catch(err => {
// do something to handle the error here
});
}, [dummyAssets]);
Or, you might find this implementation a bit simpler to follow:
useEffect(async () => {
try {
const newAssetArray = await Promise.all(dummyAssets.map(async function(asset) {
const response = await axios.get(currrentPrice(asset.asset_id));
const cp = response.data.market_data.current_price.eur;
const value = Number(cp) * Number(asset.amount);
return { ...asset, value: +value };
}));
console.log(newAssetArray);
setAssets(newAssetArray);
} catch (e) {
// do something to handle the error here
console.log(e);
}
}, [dummyAssets]);
Also, note that a structure such as:
async function someFunction() {
return await axios.get()
}
does nothing different than just:
async someFunction() {
return axios.get()
}
They both return a promise that resolves to whatever axios.get() resolves to. The first returns a promise because it's in an async function and ALL async functions return a promise at the point they hit the first await. The second returns a promise because you're directly returning the axios.get() promise. Either way, they both return a promise that resolves to the same thing.
So, in general return await fn() is not helping you vs just return fn() and is not recommended. Then, once you stop using await there, you don't need async for that .map() callback anymore either.
I am fetching data from the blockchain. Contract adresses in this case.
Once I have the adresses I fetch some info on each specific adresses and add a key=>value pair to the object. This is all working and I'm getting all the right data. However, once in the component, the newly added key=>value pair is no longer present. I think this is because the value added is a promised and the dispatch is not waiting on it. How can I fix this so the dispatch it done only once the promised is resolved.
const tokenStream = await exchange.getPastEvents('OtherToken', {fromBlock:0, toBlock: 'latest'})
const allTokens = tokenStream.map((event) => event.returnValues)
console.log('ALL TOKEN DATA : ', allTokens)
allTokens.forEach( async element => {
let symbol = await exchange.methods.getERCsymbol(element.tokenAddress).call()
element.symbol = symbol
});
console.log('ALL TOKEN DATA AFTER : ',allTokens) // I see symbol
dispatch(allTokensLoaded(allTokens))
Better solution would be to use Promise.all to wait for multiple async request/promise to finish, and also you are mixing await and then, as your main function is already async you can write it in more neat and clean way using await only.
export const loadAllTokens = async (exchange, dispatch) => {
const result = await exchange.getPastEvents('OtherToken', {fromBlock:0, toBlock: 'latest'});
const allTokens = result.map((event) => event.returnValues);
await Promise.all(allTokens.map(async (element) => {
const innerResult = await exchange.methods.getERCsymbol(element.tokenAddress).call();
element.symbol = innerResult;
element[2]= innerResult;
}));
dispatch(allTokensLoaded(allTokens));
}
its more clean and better to understand :).
if any doubts please comment.
They happens to be nested async request and this is how I fixed it
export const loadAllTokens = async (exchange, dispatch) => {
await exchange.getPastEvents('OtherToken', {fromBlock:0, toBlock: 'latest'}).then( async(result) => {
const allTokens = result.map((event) => event.returnValues)
let count = 0
allTokens.forEach( async element => {
await exchange.methods.getERCsymbol(element.tokenAddress).call().then( async(result) =>{
element.symbol = result
element[2]= result
count += 1
}).then( () => {
if(count === allTokens.length){
dispatch(allTokensLoaded(allTokens))
}
})
})
})
}
I am working on a component that reads a CSV file and sends a request per every line on the file. The process works great but I am trying to show feedback as the lines get successfully posted. The problem is that using the useState hook, the set function gets passed the moment on calling the function and not after each promise has been resolved. So I cannot append to the successful results array, the array keeps getting replaced with the last successful call.
The API calls are debounced by one second in order to prevent an overload to the server.
import React, {useState} from "react";
import CSVReader from "react-csv-reader";
import {post} from "../api";
function App() {
const [inserts, setInserts] = useState([])
const callApi = async (x) => {
let item = {
date: x.date,
value: x.value,
};
await post(`add-items`, item);
setInserts([...inserts, item])
};
const debouncedApiCall = (body, delay) => {
return new Promise((resolve) => {
const handler = () => callApi(body).then((x) => resolve(x));
setTimeout(handler, delay);
});
};
const insert = async (rows) => {
let timer = 0;
await Promise.all(
rows.map(async (x) => {
timer++;
return await debouncedApiCall(x, timer * 1000);
})
);
};
let onFileLoaded = (data) => {
insert(data).then((x) => console.log(x));
};
return (
<div>
<CSVReader onFileLoaded={onFileLoaded}/>
{JSON.stringify(inserts)}
</div>
);
}
export default App;
When your call API function is called, within its closure it captures the state of inserts. This means, that inserts is not always up to date. What you end up with is called a "stale closure".
To get around this, the mutation function provided by the useState method can accept a callback function. This callback function can recieve the latest state when calling the function. This is helpful in async operations.
your callApi function will become
const callApi = async (x) => {
let item = {
date: x.date,
value: x.value,
};
await post(`add-items`, item);
setInserts(prevState => [...prevState , item]) //prevState gets the latest state of inserts when setInserts is called
return (x); //will return this value once this async function finishes. similar to resolve(x)
};
I cant exactly debug your code, but I would think there is an unnecessary step. you should be able to change your insert function to await all your callApi's, and just return x from your callApi function (as I added above).
const insert = async (rows) => {
let timer = 0;
await Promise.all(
rows.map((x) => {
return callApi(x); //Promise.All wants an array of promises. async functions return a promise
})
);
};
As a side note, Promise.all returns a promise with the actual array results of all your promises results. You can fetch them by adding a .then to the Promise.All and remove the async from the insert function, or await the result.
Async Based: insert returns a promise, so you will need to handle that in the calling function.
const insert = async (rows) => {
let timer = 0;
const results = await Promise.all(
rows.map((x) => {
return callApi(x); //Promise.All wants an array of promises. async functions return a promise
})
);
return results; //array of all your x values for each row
};
None-async based: end of line, insert is the calling function
const insert = (rows) => {
let timer = 0;
Promise.all(
rows.map((x) => {
return callApi(x); //Promise.All wants an array of promises. async functions return a promise
})
).then((result) => {
//result is an array of all x values according to rows
});
};
I am building a react application using axios. I called this function useNames(allNames) which passes in an array of names that will be iterated by Axios. At first glance, it appears to work but when I console.log the data it appears to look like this....
function is run.....
useNames = async (allNames) => {
try {
const promisesArray = [];
await _.each((allNames), async (value) => {
const res= await axios.get(`https://pokeapi.co/api/v2/pokemon/${value}`);
promisesArray.push(res.data);
});
console.log(promisesArray);
} catch (err) {
throw new Error('Unable to insert API data');
}
}
useNames(allNames);
output...
when opened....
The array appears to have data but when I try to manipulate it, chrome declares it as empty. I have read stack overflow responses that have similar problems but I am still unable to fix it.
You're doing an await on the _.each part of the code. That will not work the way you expect it. await will wait for an async operation to complete before it moves on, however _.each is not an async operation even if the callback used is itself async. What you want to do is await the actual promises:
useNames = async (allNames) => {
try {
const promisesArray = _.map(allNames, async (value) => {
const res= await axios.get(`https://pokeapi.co/api/v2/pokemon/${value}`);
return res.data;
});
const valuesArray = await Promise.all(promisesArray);
console.log(valuesArray); // Should have resolved values
} catch (err) {
throw new Error('Unable to insert API data');
}
}
The way this works is that your async function will return a promise result which will be what the allNames array gets map into in this case. An array of promises can then be used with Promise.all which you can then await for and get all promise results.
I store some data in IndexedDB and i use npm package localforage for it.
const retrieveData = async () => {
const keys = await localforage.keys()
const data = await keys.map(async (key) => {
const item = await localforage.getItem(key)
return [item.username, item.compamy, item.email, item.lastUpdate]
})
return data
}
Whenever I execute this function, I get a resolved Promise object, which values I cannot extract
async componentDidMount() {
let asyncData = retrieveData()
console.log(asyncData) // Promise object
asyncData = retrieveData().then(values => values)
console.log(asyncData) // Promise object anyways
}
How exactly should I get data from this Promise object?
const retrieveData = async () => {
const keys = await localforage.keys()
// The return value of "keys.map" is an array of promises since
// async automatically returns a Promise behind the scenes.
// Await works on a single promise, not an array of promises,
// so "data" will not contain the actual data.
const data = await keys.map(async (key) => {
const item = await localforage.getItem(key)
return [item.username, item.compamy, item.email, item.lastUpdate]
})
return data
}
Do:
const retrieveData = async () => {
const keys = await localforage.keys()
const data = await Promise.all(keys.map(async (key) => {
const item = await localforage.getItem(key)
return [item.username, item.compamy, item.email, item.lastUpdate]
}));
return data
}
Or use Bluebird's map which works out of the box in this scenario:
// The "then" function does not do anything. It returns values,
// but only does so to the next "then" function. There are no
// further then-functions so the return value is unused.
// "values" is merely a local variable so you won't be able to
// access it anywhere outside the fat arrow function.
// You could move the console log into "then".
asyncData = retrieveData().then(values => values)
// asyncdata is still the unresolved promise object, the "then"
// function has not been run yet (then on the line above will be run when
// all of the awaits in retrieveData have been successfully resolved.
console.log(asyncData)
Do:
async componentDidMount() {
const data = await retrieveData();
console.log(data);
}
Or:
componentDidMount() {
retrieveData().then(values => {
console.log(values);
});
}
you can use for of loop here, and it will be simpler, we dont use await keyword directly in a map iterator, instead we can use promise.all as mentioned in above answer.
const retrieveData = async () => {
const keys = await localforage.keys();
let data;
for (let key of keys) {
const item = await localforage.getItem(key);
data.push([item.username, item.compamy, item.email, item.lastUpdate]);
}
return data;
}
Try using the "await" reserved keyword before your retrieveData() method at componentDidMount(), since it's a promise, an async event, you have to wait until it finishes all of it's inner executions to return some data and go on.
Just like you did at retrieveData() declaration use await before the promise. in detail what you need:
async componentDidMount() {
let asyncData = await retrieveData()
....
}