I'm using Firebase along with angularJS to fetch a list of users. I can read all the users from my database with the once() function, but I can't figure out why userList returns undefined below
.service('userService', [function() {
this.getUsers = function() {
var users;
var userList;
var ref = firebase.database().ref('/users/');
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
}).then(function(){
userList = users;
console.log(userList); // outputs all users
},(function(error){
alert('error: ' + error);
});
console.log(userList); // outputs 'undefined'
}
}]);
I wait to assign my userList variable until I'm done processing users, but no luck.
There is something important here that I am missing where Promises/callbacks are concerned and I cannot find it in the documentation; could someone help me understand my issue?
The problem is (as imjared says) that the data is read from Firebase asynchronously. So the code doesn't execute in the order that you think. It's easiest to see that by simplifying it with just a few log statements:
.service('userService', [function() {
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
console.log("before attaching listener");
ref.once('value').then(function(snapshot) {
console.log("got value");
});
console.log("after attaching listener");
}
}]);
The output of this will be:
before attaching listener
after attaching listener
got value
Knowing the order in which this executes should explain perfectly why you cannot print the user list after you've just attached the listener. If not, I recommend also reading this great answer: How to return the response from an asynchronous call
Now for the solution: you will either need to use the user list in the callback or return a promise.
Use the user list in the callback
This is the oldest way to deal with asynchronous code: move all code that needs the user list into the callback.
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
}
console.log(users); // outputs all users
})
You're reframing your code from "first load the user list, then print its contents" to "whenever the user list is loaded, print its contents". The difference in definition is minor, but suddenly you're perfectly equipped to deal with asynchronous loading.
You can also do the same with a promise, like you do in your code:
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
});
But using a promise has one huge advantage over using the callback directly: you can return a promise.
Return a promise
Often you won't want to put all the code that needs users into the getUsers() function. In that case you can either pass a callback into getUsers() (which I won't show here, but it's very similar to the callback you can pass into once()) or you can return a promise from getUsers():
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
return ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
return(users);
}).catch(function(error){
alert('error: ' + error);
});
}
With this service, we can now call getUsers() and use the resulting promise to get at the users once they're loaded:
userService.getUsers().then(function(userList) {
console.log(userList);
})
And with that you have tamed the asynchronous beast. Well.... for now at least. This will keep confusing even seasoned JavaScript developers once in a while, so don't worry if it takes some time to get used to.
Use async and await
Now that the function returns a promise, you can use async/await to make the final call from above look a bit more familiar:
function getAndLogUsers() async {
const userList = await userService.getUsers();
console.log(userList);
}
You can see that this code looks almost like a synchronous call, thanks to the use of the await keyword. But to be able to use that, we have to mark the getAndLogUsers (or whatever the parent scope of where we use await) as async, which means that it returns a Future too. So anyone calling getAndLogUsers will still need to be aware of its asynchronous nature.
You need to think async:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
console.log(users); // outputs 'undefined'
}
}]);
I bet if you did this, you'd be fine:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
window.setTimeout( function() {
console.log(users); // outputs 'undefined'
}, 1500 );
}
}]);
Clarification per Franks's comment:
I should clarify further that the setTimeout would just prove that this is an issue of timing. If you use setTimeout in your app, you're probably going to have a bad time as you can't reliably wait n milliseconds for results, you need to get them then fire a callback or resolve a promise after you have gotten the snapshot of the data.
Related
I'm using Firebase along with angularJS to fetch a list of users. I can read all the users from my database with the once() function, but I can't figure out why userList returns undefined below
.service('userService', [function() {
this.getUsers = function() {
var users;
var userList;
var ref = firebase.database().ref('/users/');
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
}).then(function(){
userList = users;
console.log(userList); // outputs all users
},(function(error){
alert('error: ' + error);
});
console.log(userList); // outputs 'undefined'
}
}]);
I wait to assign my userList variable until I'm done processing users, but no luck.
There is something important here that I am missing where Promises/callbacks are concerned and I cannot find it in the documentation; could someone help me understand my issue?
The problem is (as imjared says) that the data is read from Firebase asynchronously. So the code doesn't execute in the order that you think. It's easiest to see that by simplifying it with just a few log statements:
.service('userService', [function() {
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
console.log("before attaching listener");
ref.once('value').then(function(snapshot) {
console.log("got value");
});
console.log("after attaching listener");
}
}]);
The output of this will be:
before attaching listener
after attaching listener
got value
Knowing the order in which this executes should explain perfectly why you cannot print the user list after you've just attached the listener. If not, I recommend also reading this great answer: How to return the response from an asynchronous call
Now for the solution: you will either need to use the user list in the callback or return a promise.
Use the user list in the callback
This is the oldest way to deal with asynchronous code: move all code that needs the user list into the callback.
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
}
console.log(users); // outputs all users
})
You're reframing your code from "first load the user list, then print its contents" to "whenever the user list is loaded, print its contents". The difference in definition is minor, but suddenly you're perfectly equipped to deal with asynchronous loading.
You can also do the same with a promise, like you do in your code:
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
});
But using a promise has one huge advantage over using the callback directly: you can return a promise.
Return a promise
Often you won't want to put all the code that needs users into the getUsers() function. In that case you can either pass a callback into getUsers() (which I won't show here, but it's very similar to the callback you can pass into once()) or you can return a promise from getUsers():
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
return ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
return(users);
}).catch(function(error){
alert('error: ' + error);
});
}
With this service, we can now call getUsers() and use the resulting promise to get at the users once they're loaded:
userService.getUsers().then(function(userList) {
console.log(userList);
})
And with that you have tamed the asynchronous beast. Well.... for now at least. This will keep confusing even seasoned JavaScript developers once in a while, so don't worry if it takes some time to get used to.
Use async and await
Now that the function returns a promise, you can use async/await to make the final call from above look a bit more familiar:
function getAndLogUsers() async {
const userList = await userService.getUsers();
console.log(userList);
}
You can see that this code looks almost like a synchronous call, thanks to the use of the await keyword. But to be able to use that, we have to mark the getAndLogUsers (or whatever the parent scope of where we use await) as async, which means that it returns a Future too. So anyone calling getAndLogUsers will still need to be aware of its asynchronous nature.
You need to think async:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
console.log(users); // outputs 'undefined'
}
}]);
I bet if you did this, you'd be fine:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
window.setTimeout( function() {
console.log(users); // outputs 'undefined'
}, 1500 );
}
}]);
Clarification per Franks's comment:
I should clarify further that the setTimeout would just prove that this is an issue of timing. If you use setTimeout in your app, you're probably going to have a bad time as you can't reliably wait n milliseconds for results, you need to get them then fire a callback or resolve a promise after you have gotten the snapshot of the data.
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
I am trying to retrieve the document for specific ids in the following cloud function. The loop (see "Block Get Major") for retrieving that information is running, but the logs show that it is executing AFTER the function returns data.
I am certain this is because this creates a new promise, and that I need to tie them together.
I've looked through a lot of the posts on here and elsewhere and am just not getting it.
It looks like I am creating a broken/dropped promise. I get the necessary data, with the exception of menteeMajor, which is undefined until later in the log files).
exports.getAllMatchesMCR = functions.https.onCall((data, context) => {
var dbRef = db.collection("matches");
var dbPromise = dbRef.get();
// return the main promise
return dbPromise.then(function(querySnapshot) {
var results = [];
var idx = 0;
querySnapshot.forEach(function(doc) {
// push promise from get into results
var matchObj = {
mentorName: "",
mentorEmployer: "",
mentees: []
}
var mentor = doc.data();
mentor.mentorID = doc.id;
matchObj.mentorName = mentor.mentorID;
matchObj.mentees = mentor.mentees;
for (var curIDX in matchObj.mentees) {
matchInfoObj = {};
matchInfoObj.mentorID = matchObj.mentorID;
matchInfoObj.mentorName = matchObj.mentorName;
matchInfoObj.menteeName = matchObj.mentees[curIDX];
// Block Get Major --->
var menteeRef = db.collection('users').doc(matchObj.mentees[curIDX]);
var getDoc = menteeRef.get()
.then(doc => {
if (!doc.exists) {
console.log('No such document!');
} else {
var userInfo = {};
userInfo = doc.data();
matchInfoObj.menteeMajor = userInfo.major;
// console.log('user Major:', matchInfoObj.menteeMajor);
return userInfo.major;
}
})
.catch(err => {
// console.log('Error getting document', err);
return ("No Mentee Doc")
});
console.log("in menteeInfo: ", getDoc.data());
matchInfoObj.menteeMajor = getDoc.data(); // Block Get Major <---
if (typeof something === "undefined") {
console.log('After BLOCK Major is UNDEFINED:', matchInfoObj.menteeName);
} else {
console.log('After BLOCK :', matchInfoObj.menteeMajor);
}
results.push(matchInfoObj)
}
});
// dbPromise.then() resolves to a single promise that resolves
// once all results have resolved
return Promise.all(results)
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
});
Here are two screen shots for the logs that show the order based on console.log output. Keep in mind that the sort of these log records is NEWEST at the top. Result 1 shows the start, from deployment of function and it's start, Result 2 shows the messages from the broken promise almost two minutes later.
The plain text version of what I'm trying to do is:
1. collection "Users" contains information on Mentees and Mentors
2. collection "Matches" is a doc of Mentees (array) for each Mentor, or One to Many).
3. Display one row for EACH mentor/mentee connection. I have the ids, but I need to get the names and other information for both mentees and mentors.
Plain old Result file shows that I am getting the rows I need with the Mentor ID, and the Mentee ID (labeled Mentees, that will change)
Can someone please show me the way to chain this promise?
I'm using Firebase along with angularJS to fetch a list of users. I can read all the users from my database with the once() function, but I can't figure out why userList returns undefined below
.service('userService', [function() {
this.getUsers = function() {
var users;
var userList;
var ref = firebase.database().ref('/users/');
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
}).then(function(){
userList = users;
console.log(userList); // outputs all users
},(function(error){
alert('error: ' + error);
});
console.log(userList); // outputs 'undefined'
}
}]);
I wait to assign my userList variable until I'm done processing users, but no luck.
There is something important here that I am missing where Promises/callbacks are concerned and I cannot find it in the documentation; could someone help me understand my issue?
The problem is (as imjared says) that the data is read from Firebase asynchronously. So the code doesn't execute in the order that you think. It's easiest to see that by simplifying it with just a few log statements:
.service('userService', [function() {
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
console.log("before attaching listener");
ref.once('value').then(function(snapshot) {
console.log("got value");
});
console.log("after attaching listener");
}
}]);
The output of this will be:
before attaching listener
after attaching listener
got value
Knowing the order in which this executes should explain perfectly why you cannot print the user list after you've just attached the listener. If not, I recommend also reading this great answer: How to return the response from an asynchronous call
Now for the solution: you will either need to use the user list in the callback or return a promise.
Use the user list in the callback
This is the oldest way to deal with asynchronous code: move all code that needs the user list into the callback.
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
}
console.log(users); // outputs all users
})
You're reframing your code from "first load the user list, then print its contents" to "whenever the user list is loaded, print its contents". The difference in definition is minor, but suddenly you're perfectly equipped to deal with asynchronous loading.
You can also do the same with a promise, like you do in your code:
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
});
But using a promise has one huge advantage over using the callback directly: you can return a promise.
Return a promise
Often you won't want to put all the code that needs users into the getUsers() function. In that case you can either pass a callback into getUsers() (which I won't show here, but it's very similar to the callback you can pass into once()) or you can return a promise from getUsers():
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
return ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
return(users);
}).catch(function(error){
alert('error: ' + error);
});
}
With this service, we can now call getUsers() and use the resulting promise to get at the users once they're loaded:
userService.getUsers().then(function(userList) {
console.log(userList);
})
And with that you have tamed the asynchronous beast. Well.... for now at least. This will keep confusing even seasoned JavaScript developers once in a while, so don't worry if it takes some time to get used to.
Use async and await
Now that the function returns a promise, you can use async/await to make the final call from above look a bit more familiar:
function getAndLogUsers() async {
const userList = await userService.getUsers();
console.log(userList);
}
You can see that this code looks almost like a synchronous call, thanks to the use of the await keyword. But to be able to use that, we have to mark the getAndLogUsers (or whatever the parent scope of where we use await) as async, which means that it returns a Future too. So anyone calling getAndLogUsers will still need to be aware of its asynchronous nature.
You need to think async:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
console.log(users); // outputs 'undefined'
}
}]);
I bet if you did this, you'd be fine:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
window.setTimeout( function() {
console.log(users); // outputs 'undefined'
}, 1500 );
}
}]);
Clarification per Franks's comment:
I should clarify further that the setTimeout would just prove that this is an issue of timing. If you use setTimeout in your app, you're probably going to have a bad time as you can't reliably wait n milliseconds for results, you need to get them then fire a callback or resolve a promise after you have gotten the snapshot of the data.
The following code supposed to be update username in the data base then retrieve updated username.
updateUserMame and getUserName are two different REST calls.
updateName(name) {
var obj = this;
if (name === 'None') {
name = null;
}
obj.UtilityService.updateUserName(name, obj.userId)
.success(function (data) {
if (data) {
obj.getUserName(obj.userId);
console.log('Name is updated for ID:'||obj.userId);
} else {
console.log('Something Wrong');
}
});
}
getUserName(userId){
obj.UtilityService.getUserName(userId)
.then(function (result) {
console.log(result.user.userId);
}
}
I have user name 'Nathan Drake' in the dataBase.
When I run the update function with 'Elena Fisher', it is returning 'Nathan Drake'.
I've read some articles to make synchronus service calls, but unable to figure out what is going wrong.
Please help.
You could wrap your update function in a promise:
var updatePromise = $q.when(updateName(name)); // creates a promise
When your promise has finished processing, you can resolve it using then() which takes a success callback and an error callback
updatePromise().then(function successCallback(response){ // resolves the promise using then
getUserName(userId) // execute the rest of your code
},
function errorCallback(response){
console.log(error)
});
You would need to inject $q into the scope you are working with
Your code does not make much sense, that is I see possible mistakes as it looks like you are interchanging user name and user id and calling the obj context from inside a function even when its not declared there etc. Either we are missing code or this will fail when you try to run it.
Here is your example with some fixes and comments that show how you could do it using callbacks (no sync code, as mentioned by everyone else on this thread you should avoid actually waiting for I/O and use callbacks instead).
updateName(name) {
var obj = this; // good, you captured this
if (name === 'None') {
name = null;
}
obj.UtilityService.updateUserName(name, obj.userId)
.success(function (data) {
if (data) {
// ok, you successfully updated the name so why would you go back to the server and get it again? You know the value based on your update.
console.log('Name is updated for ID:' + obj.userId.toString());
// for your example though here is how you could handle it
obj.getUserName(obj, obj.userId, function(user){ // i assumed the name is stored in variable userName
console.log('Name from server = ' + user.userName); // no idea what you are returning but you can figure it out from here
// maybe you also want to capture it again??
obj.name = user.userName;
});
} else {
console.log('Something Wrong');
}
});
}
// pass in captured this as obj, the user id, and a callback
getUserName(obj, userId, callback){
obj.UtilityService.getUserName(userId)
.then(function (result) {
callback(result); // call the callback with the result. The caller can then do something with it
}
}