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.