Using Express, I am using .map to create an array of API calls, whose results I want to combine into one response object. Since every call uses different query parameters, I would like to use the query parameters as the response object's keys.
I create the GET requests using axios, which returns a promise, and then I use axios.all to wait for all of the promises to resolve.
The problem is that after the promises resolve, I no longer have access to the variables used to create them. How can I attach those variables to the promises for later reference?
Here is the API:
router.get('/api', (req, res) => {
const number = req.query.number;
res.json({ content: "Result for " + number });
});
Here is where I am trying to combine the results:
router.get('/array', async (req, res) => {
res.locals.payload = {};
const arr = [1, 2, 3, 4, 5];
const promises = arr.map(number => {
return axiosInstance.get('/api', { params: { number: number } })
});
const results = await axios.all(promises);
results.map(r => {
// number should match original, but I no longer have
// access to the original variable
number = 1;
res.locals.payload[number] = r.data;
});
res.json(res.locals.payload);
});
Result of GET on /array:
{
"1": {
"content": "Result for 5"
}
}
What can I do when creating the Promise objects to preserve the keys?
If the result were going to be an array with 0-based indexes instead of an object with properties "1", "2", etc., we could solve it with Promise.all (or axios.all if it offers the same guarantee Promise.all does that the array it gives you will be in the order of the promises you give it, regardless of the order in which they resolve). But I'm guessing your "number" is really a placeholder for something more interesting. :-)
You can do this by making use of the fact that a promise is a pipeline where the various handlers transform the contents along the way.
In this case, you can transform the result of the get call into an object with the number and result:
const promises = arr.map(number => {
return axiosInstance.get('/api', { params: { number } })
.then(result => ({result, number}));
});
Now, results in your all.then callback receives an array of objects with result and number properties. You could use destructuring parameters to receive them in your results.map callback if you like:
// vvvvvvvvvvvvvvvv---------------- destructuring parameters
results.map(({result, number}) => {
res.locals.payload[number] = result.data;
// ^^^^^^----^^^^^^-------- using them
});
Live Example:
// Fake axiosInstance
const axiosInstance = {
get(url, options) {
return new Promise(resolve => {
setTimeout(() => {
resolve({data: `Data for #${options.params.number}`});
}, 300 + Math.floor(Math.random() * 500));
});
}
};
// Fake axios.all:
const axios = {
all: Promise.all.bind(Promise)
};
// Fake router.get callback:
async function routerGet() {
const payload = {}; // stand-in for res.locals.payload
const arr = [1, 2, 3, 4, 5];
const promises = arr.map(
number => axiosInstance.get('/api', { params: { number } })
.then(result => ({result, number}))
);
const results = await axios.all(promises);
results.map(({result, number}) => {
/*res.locals.*/payload[number] = result.data;
});
console.log(/*res.locals.*/payload);
}
// Test it
routerGet().catch(e => { console.error(e); });
Note I've used ES2015+ property shorthand above, since you're using arrow functions and such. The object initializer { number } is exactly the same as { number: number }.
Side note 2: You can use a concise arrow function for the map callback if you like:
const promises = arr.map(
number => axiosInstance.get('/api', { params: { number } })
.then(result => ({result, number}))
);
Related
I'm still a little unfamiliar with asynchronous functions beyond their simplest forms, so I'm hoping someone here can help me out.
Some information:
getCollection() returns an array of objects that look like this.
{
"id": 1,
"userId": 3,
"gameId": 3498,
"review": "Testing review for game 1"
},
getGameById() takes in a integer (the id of a game) and returns that game object from an external API.
gameArray should be getting filled with game objects whose IDs in the external API match the IDs from the array of objects provided by getCollection.
const [games, setGames] = useState([])
const getGames = () => {
getCollection().then(userGames => {
let gameArray = []
for (const eachObj of userGames) {
if (eachObj.userId === currentUserId) {
getGameById(eachObj.gameId).then(game => {
gameArray.push(game)
})
}
}
Promise.all(gameArray).then(() => {
console.log(gameArray) //logs empty array, but shouldn't be empty
setGames(gameArray)
})
})
}
I have never used Promise.all before, it's just something I saw as a possible solution to this issue I'm having.
Promise.all takes an array of promises.
First you must build the array of promises. Then you must call Promise.all with this array to retrieve all the games :
function getGames() {
getCollection().then(userGames => {
const gamePromises = userGames
.filter(userGame => userGame.userId == currenUserId)
.map(userGame => getGameById(userGame.gameId));
Promise.all(gamePromises).then(games=> {
console.log(games);
setGames(games)
});
})
}
Here is another solution using async function which is maybe more readable
async function getGames() {
const userGames = await getCollection();
const currentUserGames = userGames.filter(({userId}) => userId == currentUserId);
const games = await Promise.all(userGames.map(({gameId}) => getGameById(gameId));
setGames(games);
}
The array that you pass to Promise.all needs to contain the promises, but you're pushing the game objects - and you're doing it asynchronously, so the array is still empty when you pass it to Promise.all.
const getGames = () => {
getCollection().then(userGames => {
let gamePromises = []
for (const eachObj of userGames) {
if (eachObj.userId === currentUserId) {
gamePromises.push(getGameById(eachObj.gameId))
// ^^^^^^^^^^^^^^^^^^ ^
}
}
return Promise.all(gamePromises)
// ^^^^^^ chaining
}).then(gameArray => {
// ^^^^^^^^^
console.log(gameArray)
setGames(gameArray)
})
}
To simplify:
async function getGames() {
const userGames = await getCollection()
const gamePromises = userGames
.filter(eachObj => eachObj.userId === currentUserId)
.map(eachObj => getGameById(eachObj.gameId))
const gameArray = await Promise.all(gamePromises)
console.log(gameArray)
setGames(gameArray)
}
I'm creating a JS function that will make a call to an API, loop through the returned data and perform another call to retrieve more information about the initial data (for example where the first call return an ID, the second call would return the name/address/number the ID corresponds to). Positioning the async and await keywords though, have proven to be way more challenging than I imagined:
useEffect(() => {
const getAppointments = async () => {
try {
const { data } = await fetchContext.authAxios.get('/appointments/' + auth.authState.id);
const updatedData = await data.map(value => {
const { data } = fetchContext.authAxios.get('/customerID/' + value.customerID);
return {
...value, // de-structuring
customerID: data
}
}
);
setAppointments(updatedData);
} catch (err) {
console.log(err);
}
};
getAppointments();
}, [fetchContext]);
Everything get displayed besides the customerID, that results undefined. I tried to position and add the async/await keywords in different places, nothing works. What am I missing?
map returns an array, not a promise. You need to get an array of promises and then solve it (also, if your way worked, it would be inefficient waitting for a request to then start the next one.)
const promises = data.map(async (value) => {
const { data } = await fetchContext.authAxios.get('/customerID/' + value.customerID);
return {
...value,
customerID: data
};
});
const updatedData = await Promise.all(promises);
Basically I have an array of objects and based from that array's length, I need to do a mutation for each instance like so:
arrayOfObjects.forEach((object) => {
someGraphQlMutation({
variables: object.id,
});
});
-----------------
const [someGraphQlMutation] = useMutation(
SOME_GRAPHQL_MUTATION,
{
onCompleted: (data) => {
// callback
}
}
);
However, after all the mutations are done, I need to call another function. I need to wait for the iteration mutations to finish before calling the callback function. Am I able to use Promise.all in this context?
Yes, you can use Promise.all to await all the mutations:
const [someGraphQlMutation] = useMutation(SOME_GRAPHQL_MUTATION);
const doEverything = async () => {
try {
const results = await Promise.all(arrayOfObjects.map(object => {
return someGraphQlMutation({ variables: object.id });
}));
// results will be an array of execution result objects (i.e. { data, error })
// Do whatever here with the results
} catch (e) {
// Handle any errors as appropriate
}
}
I want to .map instead of for loop but I still confuse how to use it here is my data
const arr = [
{
name: "John",
subject : [
{
subject_id : 1,
score :20
}
]
},
{
name: "Doe",
subject : [
{
subject_id : 2,
score :20
}
]
}
]
I want to add subject detail into subject array
async getData() {
for (let i in arr) {
let data = arr[i];
for (let i in data.subject) {
const subject = data.subject[i];
//this line code will get subject detail
const getSubject = await this.getSubject(subject);
subject["subjectData"] = getSubject;
}
}
}
everything work fine but How can I use map instead of for loop
example for output
{
name: "Doe",
subject : [
{
subject_id : 2,
score :20,
subjectData: {
//some detail
}
}
]
}
Here is what I want to do
const newData = arr.map(async data => {
const getSubject = data.subject;
const subject = getSubject.map(async zp02 => {
const getSubject = await this.getSubject(subject);
return { getSubject };
});
return { ...data, subject };
});
my subject always return null .
the reason that I want to use map because I've read many people say It faster than for loop
Is their anyway to use map instead of for
I'm learning about clean code can you give me an Idea how to refactor code in nested for loop
Check this up https://dev.to/jhalvorson/how-do-i-use-async-await-with-array-map-1h3f.
From this article:
You can't async/await Array.map since synchronous code won't sit and wait for your asynchronous code to resolve, instead it will the fire the function and move on. This is desired behaviour as we don't want our Array.map to block the thread otherwise nothing would run whilst Array.map goes and does its thing. Knowing that is cool, however, it doesn't really help us if we need to await our Array.map().
Thankfully there is a solution, we can achieve what we want by utilising Promise.all
//For example, the following won't work:
const postIds = ['123', 'dn28e29', 'dn22je3'];
const posts = await postIds.map(id => {
return axios
.get(`https://example.com/post/${id}`)
.then(res => res.data)
.catch(e => console.error(e));
}
console.log(posts) // [] it returns the promise, not the results 💩
//But this will:
const postIds = ['123', 'dn28e29', 'dn22je3'];
const posts = posts.map(post => {
return axios
.get(`https://example.com/post/${post.id}`)
.then(res => res.data)
.catch(e => console.error(e));
}
Promise.all(posts).then(res => console.log(`We have posts: ${res}!`));
Instead of immediately returning the Array.map we're assigning it to a variable and passing that variable to Promise.all, once that resolves we then have access to the new array returned by Array.map.
I'm trying to understand why the following two code blocks yield different results.
Code Block one works as expected and returns an array of the providers looked up from the database. On the other hand, Code Block two returns an array of functions. I feel like I'm missing something simple here in my understanding of Promise.all() and async / await.
The differences in the code blocks are:
Block 1: Array of promise functions is created and then wrapped in async functions using the map operator.
Block 2: Array of promises functions are created as async functions. Therefore, the map operator isn't called.
In case you aren't familiar with the Sequelize library, the findOne() method that gets called returns a promise.
Also worth mentioning, I know that I could use a single find query with and "name in" where clause to get the same results without creating an array of promises for multiple select queries. I'm doing this simply as a learning exercise on async / await and Promise.all().
CODE BLOCK 1: Using map() inside Promise.all()
private async createProfilePromises(profiles){
let profileProviderFindPromises = [];
//Build the Profile Providers Promises Array.
profiles.forEach(profile => {
profileProviderFindPromises.push(
() => {
return BaseRoute.db.models.ProfileProvider.findOne({
where: {
name: {[BaseRoute.Op.eq]: profile.profileProvider}
}
})}
);
});
//Map and Execute the Promises
let providers = await Promise.all(profileProviderFindPromises.map(async (myPromise) =>{
try{
return await myPromise();
}catch(err){
return err.toString();
}
}));
//Log the Results
console.log(providers);
}
CODE BLOCK 2: Adding the async function without the use of map()
private async createProfilePromises(profiles){
let profileProviderFindPromises = [];
//Build the Profile Providers Promises Array.
profiles.forEach(profile => {
profileProviderFindPromises.push(
async () => {
try{
return await BaseRoute.db.models.ProfileProvider.findOne({
where: {
name: {[BaseRoute.Op.eq]: profile.profileProvider}
}
});
}catch(e){
return e.toString();
}
}
);
});
//Execute the Promises
let providers = await Promise.all(profileProviderFindPromises);
//Log the Results
console.log(providers);
}
Your code basically boils down to:
const array = [1, 2, 3];
function fn() { return 1; }
array.map(fn); // [1, 1, 1]
array.push(fn);
console.log(array); // [1, 2, 3, fn]
You push a function (whether that is async or not does not matter), instead you want to push the result of calling the function:
array.push(fn());
or in your case:
array.push((async () => { /*...*/ })());
How I'd write your code:
return Promise.all(profiles.map(async profile => {
try{
return await BaseRoute.db.models.ProfileProvider.findOne({
where: {
name: { [BaseRoute.Op.eq]: profile.profileProvider }
}
});
} catch(e) {
// seriously: does that make sense? :
return e.toString();
}
}));