I know there are tons of questions and answers on using Javascript promises to load returned Firebase objects, but I haven't succeeded in capturing the data in an array.
The issue: I've set up an array (productArray below) to iteratively take in values of a Firebase snapshot, but the array remains empty after exiting the forEach loop.
database.js:
getProductList = function(){
let productArray =[];
loadFBData = function(){
firebase.initializeApp(config);
return firebase.database().ref().once("value", function(snapshot){
return snapshot.val();
});
}
Promise.all([loadFBData()]).then(function(snapshot) {
snapshot.forEach(function(product) {
productArray.push(product.val());
});
});
}
Question: I think I'm using Promise.all wrong, but can't figure out how (also am new asynchronous functions). Thanks for any hints on how to get productArray to successfully load.
Edit: To elaborate, it seems snapshot is loading with the correct Firebase data, and the forEach is correctly looping through each item in snapshot but productArray becomes (or remains) empty outside of the Promise.all statement.
Update I implemented all the suggestions below and yet, the program will not stop long enough to build an array from the snapshot object. Last year I used Angularfire's $loaded function which was so effective in getting the program to wait so I don't understand why an ordinary JS promise won't work here. Does it make sense to resort to timeout? I even tried to combine alexmac's and Pointy's suggestions, but no cigar:
getProductList = function(){
let productArray =[];
loadFBData = function(){
firebase.initializeApp(config);
return new Promise(resolve => {
firebase.database().ref().once('value')
.then(function(snapshot){
return resolve(function(snapshot){
productArray = Object.keys(snapshot).map((key, prod) => prod);
});
});
});
}
loadFBData();
}
Background
I think you're missing an understanding of both promises and the Firebase SDK.
First, note that all Firebase SDK functions return promises, so there's no need to create your own. Second, Promise.all is for combining multiple promises. A single promises can be chained off of with a simple .then().
I have a video on saving and querying data here: Save and Query Firebase Data
I have another video on promises here: Promises
Loading Firebase
You should be initializing firebase at the very top of your page. It only needs to be initialized once, so don't initialize it within a function.
You can call firebase.initializeApp(config) just after loading the Firebase SDK on the page. Or you can reference Firebase Hosting's init.js as shown below.
Example
The following example loads the Firebase SDK, uses my testing db's init.js file to initialize firebase, then queries, manipulates and returns an array.
Note that Firebase doesn't support zero-indexed arrays, so everything you get back from Firebase will be an unsorted object. This example shows a few different ways of returning the data :)
<html>
<head>
<script src="https://www.gstatic.com/firebasejs/4.3.0/firebase.js"></script>
<script src="https://quiver-two.firebaseapp.com/__/firebase/init.js"></script>
<script>
function loadFirebaseArray(path) {
return firebase.database().ref(path).once('value').then(snap => snap.val());
}
function toArray(items) {
return Object.keys(items).reduce((array, key) => {
const value = items[key];
array.push({key, value});
return array;
}, []).sort();
}
loadFirebaseArray('stackoverflow').then(items => {
console.log('items', items);
const asArray = toArray(items);
alert(JSON.stringify(asArray));
const justTheValues = asArray.map(x => x.value).sort();
alert(justTheValues);
});
</script>
</head>
</html>
A working example might look like this:
getProductList = () => {
let loadFBData = () => {
firebase.initializeApp(config);
return new Promise(resolve => {
firebase.database().ref().once('value', resolve);
});
}
let productArray =[];
loadFBData()
.then(snapshot => {
productArray = Object.keys(snapshot).map((key, prod) => prod);
}
Here, I use promisification to create a promise for firebase query. So loadFBData returns a promise, I can create a promise chain or use ES7 async/await to wait until promise will be fulfilled to use result.
Related
I try to push an array that I got from firestore, but when I try it's undefined
this is the code
const tmp = [];
const ref = firestore.collection("users").doc(user?.uid);
firestore
.collection("countrys")
.get()
.then((x) => {
x.forEach((y) => {
tmp.push(y.data());
});
});
console.log(tmp); //work
console.log(tmp[0]); //no work
this is the result from the console
as I see it's not same like another array
another array like (3) [{…}, {…}, {…}] ,but my array just show []
can someone explain to me why that's not working? thanks!
The issue here is that you've mixed asynchronous code with synchronous code.
To clarify further, firestore.collection("countrys").get() is a promise.
So if you want to interact with the data it returns just do it in your .then()
firestore
.collection("countrys")
.get()
.then((querySnapshot) => {
const tmp = [];
querySnapshot.forEach((doc) => {
tmp.push(doc.data());
});
console.log(tmp);
console.log(tmp[0]);
});
I would suggest doing some further reading into promises as well.
the console.log(temp) will execute before the
x.forEach((y) => {
tmp.push(y.data());
});
code will occur.
You need to add another .then chained to the then method so it will execute when the promise resolve to make the console log succeeded.
This is part of the event loop in JS.
I recommend to read more about it here:
https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5
And more about promises here:
https://web.dev/promises/
That is my first post here. I am not well skilled in asynchronous code so can not resolve the problem by myself
In a React/Redux app I have added cache. The idea behind it is to have something like 'Favorites' functionality but on clients' computer. So, I would like to store over there some data about books. While writing to cache works I can not successively dispatch data to store> Now my code looks like this:
export function fetchFromFavorites() {
return async (dispatch, getState) => {
const URL = getState().books.currentURL;
const Cache = await caches.open(URL);
Cache.matchAll()
.then(function (response) {
const ar = [];
response.forEach(async item => ar.push(await item.json()));
return ar;
})
.then(response => dispatch(test(response)));
};
}
In the code above test is an action that only sets the state field with payload. While the payload can be log-consoled from reducer, I can not perform on that any further action with another external function, well-checked on that kind of data. Besides DevTools mark it with blue 'i' what indicates that it has been calculated very lately. What is wrong with that code? BTW - it has nothing to do with service workers it is just inside regular React.
The function you are passing to response.forEach is returning a promise. You'd need to wait for all of those promises to resolve before returning ar.
For example, you may use something like:
// await all promises to be fulfilled before proceeding.
// note that we use response.map instead of forEach as
// we want to retain a reference to the promise returned
// by the callback.
// Additionally, we can just return the promise returned
// by `item.json()`
await Promise.all(response.map(item => item.json());
Remember, any function marked as async will return a promise wrapping the function's return type.
Note that you're mixing async/await with older style then/catch promises here. For consistency and ease of reading, you may want to use one style consistently.
here is what I am trying to do with firebase cloud function:
-Listen to any change in one of the documents under 'user' collection.
-Update carbon copies of the userinfo in the relevant documents in both 'comment' and 'post' collections.
Because I will need to query in relevant documents and update them at once, I am writing codes for transaction operations.
Here is the code that I wrote. It returns the error message, 'Function returned undefined, expected Promise or value'.
exports.useInfoUpdate = functions.firestore.document('user/{userid}').onUpdate((change,context) => {
const olduserinfo=change.before.data();
const newuserinfo=change.after.data();
db.runTransaction(t=>{
return t.get(db.collection('comment').where('userinfo','==',olduserinfo))
.then((querysnapshot)=>{
querysnapshot.forEach((doc)=>{
doc.ref.update({userinfo:newuserinfo})
})
})
})
.then(()=>{
db.runTransaction(t=>{
return t.get(db.collection('post').where('userinfo','==',olduserinfo))
.then((querysnapshot)=>{
querysnapshot.forEach((doc)=>{
doc.ref.update({userinfo:newuserinfo})
})
})
})
})
});
I am a bit confused because as far as I know, 'update' method returns a promise? I might be missing something big but I picked up programming only last November, so don't be too harsh. :)
Any advice on how to fix this issue? Thanks!
EDIT:
Building on Renaud's excellent answer, I created the below code in case someone may need it.
The complication with transaction is that the same data may be stored under different indices or in different formats. e.g. The same 'map' variable can be stored under an index in one collection, and as part of an array in another. In this case, each document returned by querying needs different update methods.
I resolved this issue using doc.ref.path, split, and switch methods. This enables application of different update methods based on the collection name. In a nutshell, something like this:
return db.runTransaction(t => {
return t.getAll(...refs)
.then(docs => {
docs.forEach(doc => {
switch (doc.ref.path.split('/')[0]) { //This returns the collection name and switch method assigns a relevant operation to be done.
case 'A':
t = t.update(doc.ref, **do whatever is needed for this collection**)
break;
case 'B':
t = t.update(doc.ref, **do whatever is needed for this collection**)
break;
default:
t = t.update(doc.ref, **do whatever is needed for this collection**)
}
})
})
})
Hope this helps!
Preamble: This is a very interesting use case!!
The problem identified by the error message comes from the fact that you don't return the Promise returned by the runTransaction() method. However there are several other problems in your code.
With the Node.js Server SDK you can indeed pass a query to the transaction's get() method (you cannot with the JavaScript SDK). However, in your case you want to update the documents returned by two queries. You cannot call twice db.runTransaction() because, then, it is not a unique transaction anymore.
So you need to use the getAll() method by passing an unpacked array of DocumentReferences. (Again, note that this getAll() method is only available in the Node.js Server SDK and not in the JavaScript SDK).
The following code will do the trick.
We run the two queries and transform the result in one array of DocumentReferences. Then we call the runTransaction() method and use the spread operator to unpack the array of DocumentReferences and pass it to the getAll() method.
Then we loop over the docs and we chain the calls to the transaction's update() method, since it returns the transaction.
However note that, with this approach, if the results of one of the two original queries change during the transaction, any new or removed documents will not be seen by the transaction.
exports.useInfoUpdate = functions.firestore.document('user/{userid}').onUpdate((change, context) => {
const olduserinfo = change.before.data();
const newuserinfo = change.after.data();
const db = admin.firestore();
const q1 = db.collection('comment').where('userinfo', '==', olduserinfo); // See the remark below: you probably need to use a document field here (e.g. olduserinfo.userinfo)
const q2 = db.collection('post').where('userinfo', '==', olduserinfo);
return Promise.all([q1.get(), q2.get()])
.then(results => {
refs = [];
results.forEach(querySnapshot => {
querySnapshot.forEach(documentSnapshot => {
refs.push(documentSnapshot.ref);
})
});
return db.runTransaction(t => {
return t.getAll(...refs)
.then(docs => {
docs.forEach(doc => {
t = t.update(doc.ref, { userinfo: newuserinfo })
})
})
})
})
});
Two last remarks:
I am not sure that db.collection('comment').where('userinfo', '==', olduserinfo); will be valid as olduserinfo is obtained through change.before.data(). You probably need to specify one field. This is probably the same for newuserinfo.
Note that you cannot do doc.ref.update() in a transaction, you need to call the transaction's update() method, not the one of a DocumentReference.
After going through simple examples on cloud functions that worked, I want to test something more complex. I have created a table (BTDevices) table in the Firebase realtime database, populated with few data:
Now I want my cloud function to use the MAC addresses for retrieving the "userId" fields. This is a snippet of my JavaScript cloud function:
var getUserIdPromisesArray = [];
for (var i = 0; i < nearbyMACsArray.length; i++) {
var tmpNearbyMAC = nearbyMACsArray[i];
var promise = admin.database().ref(`${DB_BT_DEVICES}/${tmpNearbyMAC}/userId`).once('value');
getUserIdPromisesArray.push(promise);
}
return Promise.all(getUserIdPromisesArray).then(results => {
results.forEach(result => {
var userId = result.val();
console.log('result.val() = ', result.val());
});
});
From other posts like this, this, and this, I know that I need to first create all my promises, and when they are done, then process the fetched data. So the for-loop iterates the nearbyMACsArray that contains the MAC addresses, creates a promise, and pushes it to the getUserIdPromisesArray.
Then I wait for all promises to finish and iterate the "results" array to print the read value, but the cloud function log shows a null value in one of the outputs:
Any ideas on why this is happening? Am I missing something with the promises?
Thanks.
According to the documentation, val() returns null when there is no data at the requested location. This is most likely what's going on. It's not likely a problem with your promises.
I'm working with Cloud Functions for Firebase, and I get a timeout with some of my functions. I'm pretty new with JavaScript. It looks like I need to put a for inside a promise, and I get some problems. The code actually get off from for too early, and I think he make this in a long time. Do you have some way to improve this code and make the code faster?
exports.firebaseFunctions = functions.database.ref("mess/{pushId}").onUpdate(event => {
//first i get event val and a object inside a firebase
const original = event.data.val();
const users = original.uids; // THIS ITS ALL USERS UIDS!!
// so fist i get all users uids and put inside a array
let usersUids = [];
for (let key in users) {
usersUids.push(users[key]);
}
// so now i gonna make a promise for use all this uids and get token's device
//and save them inside a another child in firebase!!
return new Promise((resolve) => {
let userTokens = [];
usersUids.forEach(element => {
admin.database().ref('users/' + element).child('token').once('value', snapShot => {
if (snapShot.val()) { // if token exist put him inside a array
userTokens.push(snapShot.val());
}
})
})
resolve({
userTokens
})
}) // now i make then here, from get userTokens and save in another child inside a firebase database
.then((res) => {
return admin.database().ref("USERS/TOKENS").push({
userTokens: res,
})
})
})
You are making network requests with firebase, so maybe that's why it's slow. You are making one request per user, so if you have 100 ids there, it might as well take a while.
But there's another problem that I notice, that is: you are just resolving to an empty list. To wait for several promises, create an array of promises, and then use Promise.all to create a promise that waits for all of them in parallel.
When you call resolve, you have already done the forEach, and you have started every promise, but they have not been added to the list yet. To make it better, chance it to a map and collect all the returned promises, and then return Promise.all.