get data from array of objects containg promises - javascript

In my react app I have some user ids and need to fetch their emails and names from different endpoints so what i do is:
const promises = ids.map(
id => ( {email: axios.get(`blabla/${id}/email`), name: axios.get(`blabla/${id}/name`)} )
);
and it gets me back:
[
{email: Promise, name: Promise},{email: Promise, name: Promise},{email: Promise, name: Promise},...
]
Now to get the data what i do is:
const results = [];
promises.map(({ email, name}) =>
Promise.all([email, name]).then((result) => {
results.push({
email: result[0].data,
name: result[1].data,
});
})
);
but i have a feeling that it may not be a good way to it, i mean it works now but i don't want to get into any issues later !! haha, for instance, a race between promises, or for instance, a user email set for another user. I don't know if they are even possible to happen but I need to check with you experts that if you confirm this way or do you suggest something else.

It's fine other than that you're not using the array map creates. Any time you're not using map's return value, use a loop (or forEach) instead. Also, any time you're writing to an array you're closing over from asynchronous operation results, it sets you up to accidentally use that array before it's populated
In this case, though, I'd use the array map gives back:
Promise.all(
promises.map(({ email, name}) =>
Promise.all([email, name])
.then(([email, name]) => ({email, name}))
)
).then(results => {
// ...use results here, it's an array of {email, name} objects
})
.catch(error => {
// ...handle error here, at least one operation failed...
});

I'd be doing it like this.
const promises = ids.map(
id => ( [axios.get(`blabla/${id}/email`), axios.get(`blabla/${id}/name`)] )
);
Promise.all(promises.map(Promise.all).then(
([email, name]) => {
console.log(email, name)
}
)).then(values => {
console.log(values);
});

Related

How to wait promises ad push data to array in forEach loop [react]

So i want to use this effect:
useEffect(() => {
return db.collection('users').orderBy('lastName').onSnapshot(snap => {
const usersArray = []
snap.forEach( async doc => {
const user = doc.data()
if(user.manager) {
await Promise.all([
db.collection('users').doc(user.manager).get(),
db.collection('roles').doc(user.profile).get()
]).then(resp => {
const manager = resp[0].data()
const role = resp[1].data()
usersArray.push({
id: doc.id,
...user,
manager: `${manager.lastName} ${manager.name} ${manager.middleName}`,
role: role.profile
})
})
} else {
await db
.collection('roles')
.doc(user.profile)
.get().then(resp => {
const role = resp.data()
usersArray.push({
id: doc.id,
...user,
role: role.profile
})
})
}
usersArray.push({test: 'test'})
})
console.log(usersArray)
setAgents(usersArray)
})
}, [])
Here i'm getting list of users, then i want to get for each user data about their role in system and their managers, and update user object and push to array. After that i want to render it. But now, i'm always getting empty array. In console.log of .then blocks data is correct.
Please help to get right code
That is because the console.log part of the code is synchronously executed before any of the data is actually written into the array. forEach array property creates a non blocking callback independent of the main execution. If you want to make sure that every item in snap is processed before executing the console.log I recommend using map instead, map has a unique property of actually returning a value (as opposed to foreach). In this case that return value would be callback promise. You could just await the result of the map to make sure all items were written into the array or even better return the actual items inside of the callback.
here is example code (for simplicity i removed the usersArray.push({test: 'test'}) which i assume is unneeded)
useEffect(() => {
return db.collection('users').orderBy('lastName').onSnapshot(snap => {
const usersArrayPromise = snap.documents.map(doc => {
const user = doc.data();
if (user.manager) return Promise.all([
db.collection('users').doc(user.manager).get(),
db.collection('roles').doc(user.profile).get()
]).then(resp => {
const manager = resp[0].data();
const role = resp[1].data();
return {id: doc.id, ...user, manager: `${manager.lastName} ${manager.name} ${manager.middleName}`, role: role.profile};
});
return db.collection('roles')
.doc(user.profile)
.get().then(resp => {
const role = resp.data();
return {id: doc.id, ...user, role: role.profile}
})
});
Promise.all(usersArrayPromise).then(usersArray => {
console.log(usersArray)
setAgents(usersArray)
});
})
}, []);
Please take a note of the fact that firebase QuerySnapshot isn't an actual array but implements array like forEach property, if you want to use map you would do snap.documents.map.

Firebase Realtime Database - multiple sequencial fetch and write operations without nesting promises

I am using Firebase Realtime database for storage.
In order to update contact data for users when a "Note" is updated I need to perform some sequential operations. I wish to update/push contacts nodes to for each "affectedUser" that is included in the "Note".
Below is a crude representation of my database.
-notes
-note123456 <-- Note being updated
affectedUsers: {'L1234567890ABC': true, 'L0987654321XYZ': true} <-- affectedUsers
-users <-- Compose contact objects from here for all affectedUsers
-L1234567890ABC
name
alias
email
avatar
favouriteColour
-L0987654321XYZ
-contacts <-- Add new contacts here
-L1234567890ABC
-ABCDEFGHIJKLMNO0123 <-- Already added contact
alias
name
-L0987654321XYZ
-ABCDEFGHIJKLMNO0123 <-- Already added contact
My starting point is a list of "affectedUsers" that needs to be updated - a list of user Ids.
The desired, simplified, workflow looks like this
Iterate "affectedUsers" and compose "contact cards"
Then iterate all "affectedUsers" and add contact cards to each affectedUser
My current code
const dbRoot = snapshot.ref
affectedUsers = ['-L1234567890ABC', '-L0987654321XYZ']
let promises = [];
affectedUsers.forEach((affectedUser) => {
const ref = dbRoot.child(`users/${affectedUser}`)
promises.push(
ref.once('value', (userSnapshot)=>{
const userNodeData = userSnapshot.val()
const contactObject = {
alias: userNodeData.alias,
name: userNodeData.name
}
return contactObject
);
})
Promise.all(promises).then((contactObjects) => {
let updateContactsPromises = [] //Collect second promise chain
//Check contacts of affectedUsers
affectedUsers.forEach((userId) => {
const ref = dbRoot.child(`contacts/${userId}`)
updateContactsPromises.push(
ref.once('value', (updateUserContactsSnapshots) => {
updateUserContactsNodeData = updateUserContactsSnapshots.val()
//Remove userId from additions, prepare database update object, push data
//...
})
)
})
//Execute second, and last promise chain
Promise.all(updateContactsPromises) //Line 328
.then(()=>{
//...
})
.catch((err)=>{})
})
.then(()=>{
//...
})
.catch((err)=>{})
I realize nested promises is not a good thing - since I get warnings when performing a firebase deploy. ;)
328:9 warning Avoid nesting promises promise/no-nesting
328:9
warning Avoid nesting promises promise/no-nesting
✖ 2 problems (0 errors, 2 warnings)
How can I make sure my calls gets executed sequentially without nesting promises?
The problem is when you have a .then directly inside another .then. Usually, this can be fixed by returning the next Promise, instead of having a nested then. For example, change
prom.then(() => {
getAnotherProm().then(handleOther);
});
to
prom.then(() => {
return getAnotherProm()
})
.then(handleOther);
Here, you can return the second Promise.all to avoid nesting .thens:
Promise.all(promises)
.then((contactObjects) => {
let updateContactsPromises = [] //Collect second promise chain
//Check contacts of affectedUsers
affectedUsers.forEach((userId) => {
// ...
})
//Execute second, and last promise chain
return Promise.all(updateContactsPromises)
})
.then((updateContactsValues) => {
// handle resolve value of updateContactsPromises
})
.catch((err) => {
// handle errors
})
Remember to only .catch at a level where you can handle the error properly, and you can chain .thens together to avoid having to duplicate .catches.
You can also use .map instead of .forEach to construct the array of Promises all at once, eg:
const dbRoot = snapshot.ref
affectedUsers = ['-L1234567890ABC', '-L0987654321XYZ']
const affectedUserPromises = affectedUsers.map((affectedUser) => {
const ref = dbRoot.child(`users/${affectedUser}`)
return ref.once('value', (userSnapshot) => {
const userNodeData = userSnapshot.val()
return {
alias: userNodeData.alias,
name: userNodeData.name
};
});
});
Promise.all(affectedUserPromises).then((contactObjects) => {
// ...

Cannot figure out how to wait for Promise

I have an array with user IDs and I need to find out what name belongs to each ID and return them in an array.
I can get the user's name from the database, using knex and push them into an array, but when I try to send the data it is always an empty array.
I am not really good with Promises so can't figure out how to apply to my project.
const userId = [10,11,12,13]
let users = []
userId.map(id => {
db.select('name').from('users').where('user_id', id)
.then(user => {
users.push(user)
})
})
res.json(users)
I want the response to wait until the looping finishes and send the users array.
Your map is creating an array of undefined because your callback function doesn't return anything. If we tweak it slightly, it'll create an array of promises, which conveniently is exactly what Promise.all expects. :-) So:
const userId = [10,11,12,13]
Promise.all(
userId.map(id => db.select('name').from('users').where('user_id', id))
)
.then(users => { // `users` is an array of users, in the same order as the IDs
res.json(users);
})
.catch(error => {
// Render an error response
});
As an alternative answer, here's how you could make 1 trip to the DB for this particular query meaning you don't need to wait for multiple Promises and reduce the load on your DB
knex.raw(
'select name from users where user_id in (' + userId.map(_ => '?').join(',') + ')',
[...userId]
);
First you need to wait for all promises to finish before running res.json(...)
Second, you shouldn't mutate outside variables after promise resolving (the order by which the promises resolve will alter your output and that is not nice.
Something like this should work fine
const userId = [10,11,12,13]
// map userId array to promise array
// Promise.all aggregates a promise array into one big promise that resolves when all promises resolve (and preserves array order)
Promise.all(
userId.map(id =>
db
.select("name")
.from("users")
.where("user_id", id)
)
)
.then(users => res.json(users))
.catch(e => console.error("Error::", e));
/*handle error in the catch block*/
/* visual example of Promise.all.then block
Promise.all([ users = [
getUser(10), -> {userId: 10, ....}
getUser(11), -> {userId: 11, ....}
getUser(12) -> {userId: 12, ....}
]) ]
*/
You want Promise.all(), see here.
Try
const userId = [10,11,12,13]
let users = userId.map(id => new Promise(resolve => {
db.select('name').from('users').where('user_id', id)
.then(user => {
resolve(user)
})
}))
Promise.all(users).then(()=>res.json(users))
Here users is an array of promises. As soon as all of them are resolved, do res.json(users).

Return posted data with res.send after post request?

I have a post route that receives an array of strings from req.body. It then takes the array and queries my local MySQL database a few times and retrieves some data that I am wanting to send back to the client (react) and then store in state.
The problem I'm having is storing my data in an array to successfully send via the res.send or res.json method.
I believe it may be an issue solved with handling the scope in another manner but I am stumped on this one.
I've tried moving the location of the original array variable declaration, but at best with res.send(aisleArr), I receive only an empty array.
I also tried not declaring 'aisleArr' before the push method, thinking that it would create a global aisleArr object but to no avail.
router.post('/complete', (req, res) => {
const list = req.body;
let aisleArr = [];
list.map(item =>
db.item.findAll({
where: {'$item.name$': item}
})
.then(data => data.map( data =>
db.aisle.findAll({
where: {'$aisle.id$': data.dataValues.aisleId, '$aisle.storeId$': 2}
}).then( result => {
if(result[0]){
aisleArr.push(result[0].name)
}else{
console.log('no match')}})
)
)
)
res.send(aisleArr)
});
In the client console only an empty array is received upon res.send completing.
It looks like you may need to make sure the map iterator is complete before you send the value. Even though the return values are chained with promises, the assignment to aisleArr is not and res.send is probably firing before the iterator is finished. Using async/await would provide an easier way to ensure that assignment is complete before moving forward.
For example:
router.post('/complete', async (req, res) => {
const list = req.body;
const aisleArr = await list.map(item =>
db.item.findAll({
where: {'$item.name$': item}
})
.then(data =>
data.map(newData =>
db.aisle.findAll({
where: {'$aisle.id$': newData.dataValues.aisleId, '$aisle.storeId$': 2}
})
.then(result => {
if (result[0].name) {
return result[0].name;
} else {
console.log('no match');
}
});
);
);
).filter(aisle => aisle);
// the filter at the end will remove null values
// (when nothing is returned from the last call to .then)
res.send(aisleArr);
});
I did not run this so it may take a tweak or two, but I hope it's a good starting point. Overall, I think the main thing to watch out for is that the map is completed before you call res.send. It may be helpful to add some more console.logs to ensure that this is the issue.

Grabbing the Value of Path in Firebase With Promises

I want to grab the value of a path in Firebase. Say I do
firebaseAuth.onAuthStateChanged(user => {
var displayName =
firebase.database()
.ref('/users/' + user.uid + '/name/')
.once('value').then(snapshot => snapshot.val());
store.dispatch('checkUser', {
user
displayName
});
});
In my console I get a promise as the value of my users displayName, inside of that I see the correct value, but how do I get to it? It throws me an error when I try and set displayName to snapshot.val() in the callback.
Why can't I do this? Seems like in the documentation it's possible.
The error is thrown because the syntax is incorrect; it would need to be enclosed in a block:
.once('value').then(snapshot => { const displayName = snapshot.val(); });
However, that won't solve the problem. You need to move the dispatch call into the callback - where the snapshot has been resolved:
firebaseAuth.onAuthStateChanged(user => {
firebase.database()
.ref('/users/' + user.uid + '/name/')
.once('value').then(snapshot => store.dispatch('checkUser', {
user
displayName: snapshot.val()
}));
});
Looks like a syntax problem. Arrow functions have a few valid configurations with brackets etc. that can be tough to sort out (it still bites me sometimes after a year of using ES6).
Here's a few valid versions to consider. This isn't an exhaustive list of possible syntax combinations, but I find it covers most of my use cases:
Standard form, multi line block, brackets. This is the version you use when you have more to do than just return a value (e.g., side effects like dispatch):
(snapshot) => {
const displayname = snapshot. val()
// then you'd dispatch here
return
}
One liner, term after the arrow is assumed to be return value. No assignments on the right side in this version, and no dispatch calls:
snapshot => snapshot.val()
In-between, multiple lines enclosed by parentheses, but still just one value assumed to be return value:
snapshot => ({
displayName: snapshot.val()
})
// returns { displayName: "foo" }

Categories

Resources