Issue resolving nested promises - javascript

Below is how I generally use promises and I haven't had any issues thus far until now. The problem here is that the console.log(this.recipe) is undefined and when I console.log(JSON.stringify(recipes)) it shows me its an empty array. This leads me to believe that I am not resolving the nested promises correctly.
ngOnInit(): void {
this.recipeService.getAllRecipesByUserId().then((recipes) => {
this.allRecipes = recipes;
this.recipe = this.allRecipes[0];
console.log(this.recipe);
});
}
The getAllRecipesByUserId() function should return a promise of type Recipe[]
async getAllRecipesByUserId(): Promise<Recipe[]> {
let recipes: Recipe[] = [];
await this.userService.userRecipeIds().then((ids) => {
ids.forEach((id) => {
const q = query(this.recipeCollection, where('id', '==', id));
getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
recipes?.push(doc.data() as Recipe);
});
});
});
});
return recipes;
}
userRecipeIds():
async userRecipeIds(): Promise<string[]> {
let user: User = new User();
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
return await getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => (user = doc.data() as User));
return user.recipes;
});
}
Am I resolving the promises correctly here ?
EDIT:
I have edited the userRecipeIds() method to the following:
async userRecipeIds(): Promise<string[]> {
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
const docs = await getDocs(q);
const user = docs.docs[0].data() as User;
return user.recipes;
}
And then subsequently refactored the getAllRecipesByUserId() to the following:
async getAllRecipesByUserId(): Promise<Recipe[]> {
let recipes: Recipe[] = [];
const userRecipes = await this.userService.userRecipeIds();
userRecipes.forEach(async (recipeId) => {
const q = query(this.recipeCollection, where('id', '==', recipeId));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
recipes.push(doc.data() as Recipe);
});
});
return recipes;
}
I still seem to run into the same problem where the array seems to be empty upon inspection within the ngOnInit().

I don't fully understand your code, but for instance this snippet looks wrong. You are iterating over an array an assign each item to a single field called user, so at the end, after the forEach will take place, field user will have the value of the last item, so the last doc as you call it.
Maybe the whole method should look more like this:
async userRecipeIds(): Promise<Recipe[]> {
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
const docs = await getDocs(q);
const recipes = docs.map(doc => doc.data());
return recipes;
}
in case we only want first element:
async userRecipeIds(): Promise<User> {
const q = query(
this.userCollection,
where('userId', '==', this.auth.getCurrentUser()?.uid)
);
const docs = await getDocs(q);
const user = docs[0].data();
return user;
}
Then that's how the other function can maybe look like?:
async getAllRecipesByUserId(): Promise<Recipe[]> {
const recipes: Recipe[] = await this.userService.userRecipeIds();
return recipes;
}
Answer to edited question:
When you need to resolve multiple promises at once, for instance when the promises are created from items of an array. You need to treat the results asynchronously. The foreach method doesn't care about that and will end asap, without waiting for results from your promises inside.
What you should do is use Promise.all method. Store all promises, and resolve them all, making an array of results:
async getAllRecipesByUserId(): Promise<Recipe[]> {
const userRecipes = await this.userService.userRecipeIds();
const recipieQueries = userRecipes.map(recipe => {
const q = query(...));
return getDocs(q);
})
const recipes = await Promise.all(allRecipiesPromises);
return recipes;
}

Related

How to catch Firebase promise in React?

I have a simple function that checks if the user has Premium access or not:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
if (doc.id === auth.currentUser.uid) {
return doc.data().userSettings.hasPremium
}
});
})
}
else{
return false
}
}
I tried to catch this in various ways, but no luck, it always returns an "undefined" object.
const getPremium = async => {
checkPremium.then((response) => console.log(response))
}
const getPremium = async => {
let hasPremium = await checkPremium()
}
let hasPremium = checkPremium()
What is the correct way to get the returned Boolean value?
onSnapshot is meant for listening to a collection continuously, getting repeatedly notified as its value changes. It does not create a promise, so the promise returned by getPremium is unrelated to the data you will eventually get in onSnapshot. If you just want to get the value once, you should use getDocs:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
const querySnapshot = await getDocs(q);
const match = querySnapshot.docs.find(doc => doc.id === auth.currentUser.uid);
if (match) {
return doc.data().userSettings.hasPremium);
} else {
return false;
}
}
else{
return false
}
}
Also, instead of getting all the users and then using client side code to find the one with the right id, you could just fetch that individual doc directly:
const ref = doc(db_firestore, 'users', auth.currentUser.uid)
const snapshot = await getDoc(ref);
const data = snapshot.data();
if (data) {
return data.userSettings.hasPremium
} else {
return false
}

Firebase async/await query not working as expected

Hey guys I'm abit new to this but I'll explain it the best way I can, So I'm using a function to return a promise my code looks something like this
getAccounts(email) {
return new Promise((resolve, reject) => {
usersCollection.where('email', '==', email).where('userType', 'in', ['Admin', 'Superuser'])
.get()
.then(async querySnapshot => {
const accounts = [];
await querySnapshot.forEach(async account => {
let accountData = await account.data();
accountData.id = accountData.userType;
if (accountData.userType === 'Admin') {
const adminObj = new Admin();
const adminData = await adminObj.getAdminDetails();
accountData = { ...accountData, ...adminData };
}
accountData.uid = authId;
await accounts.push(accountData);
});
resolve(accounts);
});
});
}
I currently have two accounts, one Admin, the other Superuser the problem is the promise resolved before adminData can be fetched, what could be the issue?
You are mixing await style with .then(). Get rid of Promise and .then entirely, and stick with async.
You can't use await inside .forEach() or any other Array method (map, filter, etc) but you can inside a for loop.
accounts.push is perfectly synchronous, no need to await it at all.
const getAccounts = async email => {
const querySnapshot = await usersCollection
.where('email', '==', email)
.where('userType', 'in', ['Admin', 'Superuser'])
.get();
const accounts = [];
for( let account of querySnapshot.docs ){
let accountData = await account.data();
accountData.id = accountData.userType;
if (accountData.userType === 'Admin') {
const adminObj = new Admin();
const adminData = await adminObj.getAdminDetails();
accountData = { ...accountData, ...adminData };
}
accountData.uid = authId;
accounts.push(accountData);
}
return accounts;
}
const accounts = await getAccounts("some.email#domain.com");

Why using await is also giving a pending promise in my code?

Why I am getting pending promises even after using await in the getCart() method? Is there something conceptually wrong in my code?
Please help. Thank you!
class User {
constructor(..., cart) {
//...
this.cart = cart; //{items: []}
}
getCart() {
return (async () => {
const products = this.cart.items.map(async (item) => {
const product = await Products.getProductDetail(item.productId); //this returns a promise.
product.qty = item.qty;
return product; //<pending> Promise
});
console.log(products); //<pending> Promise
return products;
})();
}
}
Here is the function call:
exports.renderCart = (req, res, next) => {
(async () => {
const products = await req.user.getCart(); //req.user is a User class object, ignore it.
console.log(products); //pending promise :(
res.render('shop/cart', { products, pageTitle: 'Cart'});
})();
};
products in your code is an array of promises. You need to wait for them all to finish using Promise.all.
The problem can be demonstrated with a bit of a mockup
var fakeWait = x => new Promise(resolve => setTimeout(() => resolve(x), 1000));
function test(){
const items = [1,2,3,4,5]
const result = items.map( async x => await fakeWait(x));
console.log(result); // list of unresolved promises
}
async function test2(){
const items = [1,2,3,4,5]
const result = await Promise.all(items.map( async x => await fakeWait(x)));
console.log(result); // list of resolved values
}
test();
test2();
In addition, your getCart method is overly complex - it does not need to be an IIFE
getCart() {
const products = this.cart.items.map(async (item) => {
const product = await Products.getProductDetail(item.productId); //this returns a promise.
product.qty = item.qty;
return product; //<pending> Promise
});
return Promise.all(products);
}
And then simply:
const products = await req.user.getCart();

Promise inside a loop inside an async function

I am working on a project using react and firebase and redux and I have some items that did created by a user. I'm storing the id of the user in the item object so i can populate the user later when i get the item to display.
Now I'm trying to get the items and modify them by replacing the user id with the actual info about the user but I have a promises problem. In my code I just get an empty array which mean the modification didn't get resolved before I return the final result.
export const getItems = () => {
return (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const items = [];
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
return dbRef
.get()
.then((res) => {
const firstVisible = res.docs[0];
const lastVisible = res.docs[res.docs.length - 1];
async function getData(res) {
/////////////////////////////////////////////// how to finish this code befor jumping to the return line
await res.forEach((doc) => {
firestore
.collection('users')
.doc(doc.data().owner)
.get()
.then((res) => {
items.push({ ...doc.data(), owner: res.data() });
});
});
////////////////////////////////////////////////
return { docs: items, lastVisible, firstVisible };
}
return getData(res);
})
.catch((err) => {
console.log(err);
});
};
};
I don't get exactly what you are trying to do, but I would suggest putting some order to make your code easy to read and work with.
You can use for of to manage async looping. I suggest something like this, disclaimer, I did it at the eye, problably there are some errors, but you can get the idea.
const getAllDocs = function (data) {
let temp = [];
data.forEach(function (doc) {
temp.push(doc.data());
});
return { data: temp };
};
const getDoc = snap => (snap.exists ? { data: snap.data() } : {});
export const getItems = () => {
return async (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
const usersRef = firestore.collection('users');
let temps = [];
const { data: items } = await dbRef.get().then(getAllDocs);
const firstVisible = items[0];
const lastVisible = items[items.length - 1];
for (const item of items) {
const { data: user } = await usersRef.doc(item.owner).get().then(getDoc);
const owner = {
/* whatever this means*/
};
temps.push({ ...user, owner });
}
return { docs: temps, lastVisible, firstVisible };
};
};
The problem is that an array of Promises is not itself a Promise -- so awaiting it will be a no-op.
You can solve this using Promise.all if you want to load them all asynchronously.
const items = await Promise.all(res.map(async (doc) => {
const res = await firestore.collection('users').doc(doc.data().owner).get();
return { ...doc.data(), owner: res.data() };
});
Otherwise you can await in a for loop as suggested in other answers.

firestore get data from two tables one after the other return empty array

I'm using firebase - firestore. I have courses and tasks collection.
I want to get all the courses of user from courses collection and for each course to get days data from tasks collection and then save all this data in one array.
getData = () => {
var arr = []
f.auth().onAuthStateChanged(async (user) => {
db.collection("courses")
.where("uid", "==", user.uid)
.get()
.then((snapshot) => {
var a = {};
snapshot.forEach((doc) => {
let coursesData = doc.data()
let courseName = coursesData.name;
let kita = coursesData.kita;
a = { name: courseName, id: doc.data().code, k: kita };
let snapshotData = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get();
let numActiveCourse = 0;
snapshotData.forEach((dc) => {
let taskData = dc.data()
console.log('taskData',taskData)
let days = taskData.days;
if (days > 0) {
numActiveCourse = 1;
}
});
a = { ...a, numActiveCourse };
arr.push(a);
console.log("arr2 is", arr);
});
})
.catch((e) => {
console.log("error is courses", e);
});
this.setState({data:arr})
});
};
the problem is that the arr is always empty (I guess I have asyncornize issue)
and the snapshot not await after it will finish.
I found solution.
the issue it's because I tried to make async await into forEach and it not wait to answer.
the solution is
readCourses = async()=>{
f.auth().onAuthStateChanged(async (user) => {
let loadedPosts = {};
let docSnaps = await db.collection("courses").where("uid", "==", user.uid).get();
for (let doc of docSnaps.docs){
let courseName = doc.data().name;
let kita = doc.data().kita
loadedPosts[doc.id] = {
...doc.data(),
k:kita,
id:doc.data().code
}
const taskSnap = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get()
let numActiveCourse = 0
for(let task of taskSnap.docs){
let taskData = task.data()
if(taskData.days>0){
numActiveCourse =numActiveCourse+1
}
}
loadedPosts[doc.id].numActiveCourse = numActiveCourse
}
console.log('loadedPosts',loadedPosts)
this.setState({data:loadedPosts})
})
}
if you have any other solution I would like to see.
It's not a good idea to mix await and then as it was in your original code. There was your first mistake. Not only you didn't wait for results of forEach, but this.setState({data:arr}) was outside of then and executed even before you've reached the forEach call.
Another issue with your initial version of code is as you said - not waiting for results of forEach. But I'm not sure that you fully understand it. Because you didn't have to change your code so much (readability aside). All your had to do is:
// change db.collection("courses")...then(...) to
const snapshot = await db.collection("courses")... // only now onAuthStateChanged callback becomes async
...
// then change forEach() to map() and wait for result
const promises = snapshot.map(async (doc) => { ... })
await Promise.all(promises)
...
As for your new version: in each iteration of the for loop you await. That means that requests for every taskSnap will be executed one after another. That's bad. Especially on slow connections. Check out the snippet (I've simplified it to the bare minimum): getData with map completes in 1 second, version with for - in 4 seconds. (And you also removed catch from your new code - not a great idea.)
let i = 0
const get_courses = () => new Promise((resolve) => setTimeout(() => resolve(["a","b","c","d"]), 10))
const get_tasks = () => new Promise((resolve) => setTimeout(() => resolve(++i), 1000))
const f_auth_onAuthStateChanged = fn => fn()
const getData = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
const promises = courses.map(async (course) => {
const tasks = await get_tasks()
data.push({ course, tasks })
})
await Promise.all(promises)
console.log(data) // this.setState({ data })
console.timeEnd("map")
} catch(e) { console.error(e) }
})
}
console.time("map")
getData()
const getData2 = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
for (const course of courses) {
const tasks = await get_tasks()
data.push({ course, tasks })
}
console.log(data) // this.setState({ data })
console.timeEnd("for")
} catch(e) { console.error(e) }
})
}
console.time("for")
getData2()
The readCourses function from your own answer doesn't return a Promise. So formally it's not async. That won't change anything, except for a small code readability improvement. Same goes for onAuthStateChanged callback from your original code.

Categories

Resources