Javascript return function happens too late - javascript

What my code should do
I am creating a social media app using react native, and I have run into an problem with my code. I am trying to create a function which grabs all of the posts from all of the user's groups. To do this, I created a loop. The loop repeats once for every group the user is in. Every one loop gets the posts from one group. Every time the loop is called, a new function is called which gets the posts from a new group the user is in, then returns the posts back to the original function, which adds them to a full list of posts.
Problem
The function getting the posts doesn't return the posts. I think that the code is not waiting for the posts to be returned, and moves on. Basically, when I console.log the posts on the function that got them, I get the correct posts, but when I console.log the entire list of posts, I get nothing back.
My question
How can I wait for a value to be returned by a function, and not have the code instantly move on?
My code
runQ(group){
//this function actually gets the posts from the server (from firebase)
var items = [];
firebase.database().ref('posts/'+group).limitToLast(
Math.floor(24/this.state.passGroups.length)*this.state.numOfPosts
).orderByKey().once ('value', (snap) => {
snap.forEach ( (child) => {
items.push({
//post info
});
});
this.setState({passItems: items})
console.log(items); //logs correct items.
}).then(()=>{
if( this.state.passItems.length != 0 ){return this.state.passItems;}
})
}
//gets the user's groups, then sends out a request to each group for the newest posts.
getItems(){
//gets groups...
//...
.then(()=>{
var allItems = [];
//allItems will be the big list of all of the posts.
for (i = 0; i < this.state.passGroups.length; i++) {
//sending request to runQ function to get posts.
allItems.push(this.runQ(this.state.passGroups[i].name)) //allItems logs as allItems: ,,,,
}
})
}

Use async-await to make the for loop wait for each response.
First, you need to return the promise created by your Firebase call (you currently don't return anything from your runQ() function).
Change this line:
firebase.database().ref('posts/'+group).limitToLast(
into:
return firebase.database().ref('posts/'+group).limitToLast(
Then tell your callback to the getItems() call to be an async function and await each response from runQ():
getItems(){
//gets groups...
//...
.then(async () => {
var allItems = [];
for (var i = 0; i < this.state.passGroups.length; i++) {
allItems.push(await this.runQ(this.state.passGroups[i].name))
}
})
}

First you have to return a promise from runQ like this: return firebase.database.....
Then in the for loop you can do like this:
let allPromises = []
for (i = 0; i < this.state.passGroups.length; i++) {
allPromises.push(this.runQ(this.state.passGroups[i].name))
}
Promise.all(allPromises).then(itemsArray => {
allItems = allItems.concat(itemsArray)
})
Make sure that you have allItems declared in the right scope.

Related

JS: several async functions return "this value was evaluated upon first expanding"

I am using powerbi rest api to get states of slicers from the active page. To get only slicers which states are not empty I have written a simple function. However, in the console.log statement I see "this value was evaluated upon first expanding" and hence my var is empty.
//TODO: not working
async function getSlicersStates(reportObj) {
// let states = []; //Also empty
const page = await reportObj.getActivePage(); //Get page
const visuals = await page.getVisuals(); //Get visuals
visuals.forEach(async function(vis){
let states = [];
if (vis.type == "slicer") { //filter all but slicers
let state = await vis.getSlicerState(); //get state of each slicer
if (state.filters.length > 0) {
// Push state and title
states.push({ title: vis.title, data: state});
}
}
return states; //Empty == this value was evaluated upon first expanding
});
}
I have tried to put "return states;" on different places in the function but it has not helped.
How I call the function
getSlicersStates(reportObj)
.then(function(states) {
console.error(states); //this value was evaluated upon first expanding
})
.catch(function(err) {
console.error(err);
})
The message you're seeing is not related to your problem. That's a standard information message that some implementations of the console show when logging objects, to help point out that changes to the object may be included in the log.
Instead, the main issue is that you don't have a return statement in getSlicersStates. There's one inside the .forEach, but that has no effect. .forEach doesn't pay any attention to the return value. You also seem to be expecting .forEach to wait for asynchronous things to finish, but it will not do so.
If you want to have an asynchronous loop that waits, you cannot use .forEach. Here's an alternative:
async function getSlicersStates(reportObj) {
const page = await reportObj.getActivePage(); //Get page
const visuals = await page.getVisuals(); //Get visuals
let states = [];
for (const vis of visuals) {
if (vis.type == "slicer") { //filter all but slicers
let state = await vis.getSlicerState(); //get state of each slicer
if (state.filters.length > 0) {
// Push state and title
states.push({ title: vis.title, data: state});
}
}
}
return states;
}

Action after push elements in array

I have implemented a function that I use to call an api, recover for each product some info and push into array. After that I have done this for all products, I would to do a action with the array.
So I have:
newProducts: any = []
loadOfferRelated(offer) {
// consider this offer with 2 products array.
let products = offer.products
for (let i = 0; i < products.length; i++) {
let apiCall = this.offerService.apiCall(products[i].id, product.catalogId)
apiCall.pipe(untilDestroyed(this)).subscribe(
(data) => { operationOnArray(data, products[i])}
)
}
if(this.newProducts.length > 0){
// ---> Here i should call another function but it doesn't enter there)
}
operationOnArray(data, product){
// I make some operation product and save in array
this.newProducts.push(products)
console.log("this.newProducts", this.newProducts) <-- this is every for populated + 1
return
}
I have a problem to call the if when the array newProducts is populated, how can I do?
Ok so you can use a forkJoin, to make the api calls simultaneously and use the map operator to return each transformed element, push the returned data into the array and then finally call the if condition.
newProducts: any = [];
loadOfferRelated(offer) {
// consider this offer with 2 products array.
let products = offer.products;
const apiCalls = products
.map((product: any) => this.offerService.apiCall(product.id, product.catalogId))
.pipe(
map((data: any, i: number) => {
// i am not sure what your doing with the api call (data) so I am merging product and data, you customize
// to your requirement
return { ...products[i], ...data };
})
);
forkJoin(apiCalls).subscribe((newProducts: Array<any>) => {
this.newProducts = newProducts;
if (this.newProducts.length > 0) {
// ---> Here i should call another function but it doesn't enter there)
}
});
}
Your problem is that whatever happens in you subscription is asynchronous. That means that your if is triggered before your request has ended, so it's most likely that your if condition will always end with a false.

problems with an array awaiting for a function that reads from firestore

I'm trying to build a method which reads from firestore an array of elements (object):
I have a service which retrieves the data from firestore, first it gets an array of document references
var data = snapshot.get('elements');
and then it gets all the objects:
getElements(){
return new Promise(res =>{
this.AngularAuth.currentUser
.then( user => {
this.useruid = user.uid;
this.db.firestore.doc(`/users/${this.useruid}`).get().then(snapshot =>{
if(snapshot.exists){
var data = snapshot.get('elements'); //This gets the array of elements
data.forEach(element => {
this.db.firestore.doc(element).get().then(object =>{
if(object.exists){
var elem = object.data() as object;
this.array.push(elem);//I kind of push in the array instances of object
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(err =>{
console.log(err);
})
});
res(this.array);
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(function(error) {
// An error happened.
})
})
.catch(function(error) {
// An error happened.
})
});
}
Then in a component I have an async method which calls the service, and tries to push into another array all the names from each object in the first array:
async retrieveArray(){
this.array = await this.service.getElements();
this.array.forEach(element => {
this.names.push(element.name);
});
console.log(this.array);
console.log(this.names);
}
However when I look to the console, the first array (array) gives me indeed an array of objects, but the other array (names) is empty.
I used the method get to retrieve the data because I don't want to listen to it, I might need the value just once.
Personally I find the async/await syntax infinitely more elegant and easier to deal with than a good old .then() callback hell :
async getElements() {
let user;
try{
user = await this.AngularAuth.currentUser();
} catch(err) {
console.log(err);
return;
}
this.useruid = user.uid;
const snapshot = await this.db.firestore.doc(`/users/${this.useruid}`).get();
if (!snapshot.exists) {
console.log("Error. Doc doesn't exist")
return
}
const data = snapshot.get('elements'); //This gets the array of elements
let toReturn = [];
for(let element of data){ // can also use 'await Promise.all()' here instead of for...of
const object = await this.db.firestore.doc(element).get();
toReturn.push(elem);
}
return toReturn;
}
async retrieveArray(){
this.array = await this.service.getElements();
this.names = this.array.map( element => element.name ) // Also use .map() here
console.log(this.array);
console.log(this.names);
}
If you use for...of, all calls will be made one after the other, in order. If you use await Promise.all(), all calls will be made and awaited simultaneously, which is faster but recommended only if you have a small number of calls to make (otherwise this could overload the server you're calling, or even be considered as a DDoS attack.)
I think the issue is in this part of your code:
if(snapshot.exists){
var data = snapshot.get('elements'); //This gets the array of elements
data.forEach(element => {
this.db.firestore.doc(element).get().then(object =>{
if(object.exists){
var elem = object.data() as object;
this.array.push(elem);//I kind of push in the array instances of object
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(err =>{
console.log(err);
})
});
res(this.nombres);
}
You're looping through the elements and fetching the object from firebase for each one. Each time is an async call, but you're not waiting for each of these calls to finish before calling res(this.nombres).
As for why the console.log(this.array) shows a populated array is that the console can be misleading. It provides the data in a kind of 'live' way (it's a reference to the array), and sometimes by the time the data arrives on the console, it's different to what the data looked like when console.log was called.
To make sure you see the data precisely as it was when console.log was called, try this:
console.log(JSON.parse(JSON.stringify(this.array));
As for the issue with your code, you need to wait for all the elements to have been fetched before you call the resolve function of your promise. Because you don't necessarily know the order in which the responses will come back, one option is to simply have a counter of how many results are remaining (you know how many you are expecting), and once the last response has been received, call the resolve function. This is how I would do it, but obviously I can't test it so it might not work:
if(snapshot.exists){
var data = snapshot.get('elements'); //This gets the array of elements
// *** we remember the number of elements we're fetching ***
let count = data.length;
data.forEach(element => {
this.db.firestore.doc(element).get().then(object =>{
// *** decrement count ***
count--;
if(object.exists){
var elem = object.data() as object;
this.array.push(elem);//I kind of push in the array instances of object
// *** If count has reached zero, now it's time to call the response function
if (count === 0) {
res(this.nombres);
}
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(err =>{
console.log(err);
})
});
// *** remove this line because it's calling the resolve function before nombres is populated
//res(this.nombres);
}
You might want to add behaviour for when the result of snapshot.get('elements') is empty, but hopefully with this you'll be on your way to a solution.
** EDIT **
I'm keeping this up just because the console.log issue might well be useful for you to know about, but I highly recommend the async/await approach suggested by Jeremy. I agree that's it's much more readable and elegant

FIrebase "pushing" object to an array is not working

app.get('/zones/:id/experiences', function(req,res) {
var zone_key = req.params.id;
var recent = [];
var ref = firebase.database().ref('participants/'+zone_key+'/experiences');
ref.on("value", function(snapshot) {
snapshot.forEach((snap) => {
firebase.database().ref('experiences').child(snap.val()).once("value").then((usersnap) => {
recent.push(usersnap.val());
});
});
console.log(recent);
});
res.render('experiences',{key: zone_key, list: recent});
});
In the above code, I am querying a reference point to get a set of "keys". Then for each key, I am querying another reference point to get the object associated to that key. Then for each object returned for those keys, I simply want to push the objects into a list. I then want to pass in this list to the client site to do stuff with the data using the render.
For some reason, the recent [] never gets populated. It remains empty. Is this an issue with my variables not being in scope? I console logged to check what the data the reference points are returning and its all good, I get the data that I want.
P.S is nesting queries like this ok? For loop within another query
As cartant commented: the data is loaded from Firebase asynchronously; by the time you call res.render, the list will still be empty.
An easy way to see this is the 1-2-3 test:
app.get('/zones/:id/experiences', function(req,res) {
var zone_key = req.params.id;
var recent = [];
var ref = firebase.database().ref('participants/'+zone_key+'/experiences');
console.log("1");
ref.on("value", function(snapshot) {
console.log("2");
});
console.log("3");
});
When you run this code, it prints:
1
3
2
You probably expected it to print 1,2,3, but since the on("value" loads data asynchronously, that is not the case.
The solution is to move the code that needs access to the data into the callback, where the data is available. In your code you need both the original value and the joined usersnap values, so it requires a bit of work.
app.get('/zones/:id/experiences', function(req,res) {
var zone_key = req.params.id;
var recent = [];
var ref = firebase.database().ref('participants/'+zone_key+'/experiences');
ref.on("value", function(snapshot) {
var promises = [];
snapshot.forEach((snap) => {
promises.push(firebase.database().ref('experiences').child(snap.val()).once("value"));
Promise.all(promises).then((snapshots) => {
snapshots.forEach((usersnap) => {
recent.push(usersnap.val());
});
res.render('experiences',{key: zone_key, list: recent});
});
});
});
});
In this snippet we use Promise.all to wait for all usersnaps to load.

Firebase not receiving data before view loaded - empty array returned before filled

In the following code I save each item's key and an email address in one table, and to retrieve the object to fetch from the original table using said key. I can see that the items are being put into the rawList array when I console.log, but the function is returning this.cartList before it has anything in it, so the view doesn't receive any of the data. How can I make it so that this.cartList waits for rawList to be full before it is returned?
ionViewWillEnter() {
var user = firebase.auth().currentUser;
this.cartData.getCart().on('value', snapshot => {
let rawList = [];
snapshot.forEach(snap => {
if (user.email == snap.val().email) {
var desiredItem = this.goodsData.findGoodById(snap.val().key);
desiredItem.once("value")
.then(function(snapshot2) {
rawList.push(snapshot2);
});
return false
}
});
console.log(rawList);
this.cartList = rawList;
});
}
I have tried putting the this.cartList = rawList in a number of different locations (before return false, even inside the .then statement, but that did not solve the problem.
The following function call is asynchronous and you're falling out of scope before rawList has a chance to update because this database call takes a reasonably long time:
desiredItem.once("value").then(function(snapshot2) {
rawList.push(snapshot2);
});
You're also pushing the snapshot directly to this list, when you should be pushing snapshot2.val() to get the raw value.
Here's how I would fix your code:
ionViewWillEnter() {
var user = firebase.auth().currentUser;
this.cartData.getCart().on('value', snapshot => {
// clear the existing `this.cartList`
this.cartList = [];
snapshot.forEach(snap => {
if (user.email == snap.val().email) {
var desiredItem = this.goodsData.findGoodById(snap.val().key);
desiredItem.once("value")
.then(function(snapshot2) {
// push directly to the cartList
this.cartList.push(snapshot2.val());
});
}
return false;
});
});
}
The problem is the Promise (async .once() call to firebase) inside the forEach loop (sync). The forEach Loop is not gonna wait for the then() statement so then on the next iteration the data of the previous iteration is just lost...
let snapshots = [1, 2, 3];
let rawList = [];
snapshots.forEach((snap) => {
console.log(rawList.length)
fbCall = new Promise((resolve, reject) => {
setTimeout(function() {
resolve("Success!");
}, 2500)
});
fbCall.then((result) => {
rawList.push(result);
});
})
You need forEach to push the whole Promise to the rawList and Then wait for them to resolve and do sth with the results.
var snapshots = [1, 2, 3];
var rawList = [];
var counter = 0;
snapshots.forEach((snap) => {
console.log(rawList.length)
var fbCall = new Promise((resolve, reject) => {
setTimeout(function() {
resolve("Success!" + counter++);
}, 1500)
});
rawList.push(fbCall);
})
Promise.all(rawList).then((res) => {
console.log(res[0]);
console.log(res[1]);
console.log(res[2]);
});
The thing is, it is still a bit awkward to assign this.cartList = Promise.all(rawList) as it makes it a Promise. So you might want to rethink your design and make something like a getCartList Service? (dont know what ur app is like :p)
Since you're using angular you should also be using angularfire2, which makes use of Observables which will solve this issue for you. You will still be using the normal SDK for many things but for fetching and binding data it is not recommended to use Firebase alone without angularfire2 as it makes these things less manageable.
The nice things about this approach is that you can leverage any methods on Observable such as filter, first, map etc.
After installing it simply do:
public items$: FirebaseListObservable<any[]>;
this.items$ = this.af.database.list('path/to/data');
And in the view:
{{items$ | async}}
In order to wait for the data to appear.
Use AngularFire2 and RxJS this will save you a lot of time, and you will do it in the proper and maintainable way by using the RxJS operators, you can learn about those operators here learnrxjs

Categories

Resources