This question already has an answer here:
How to get data from firestore DB in outside of onSnapshot
(1 answer)
Closed 3 years ago.
I am using Firebase's Cloud Firestore for a web page I'm working on. I have it currently setup to create a new document in the "Users" collection when a new user is added/joined. The issue is when I try to pull the list of users down to iterate over them, I'm not able to.
I have tried iterating over it wither different kinds of loops. The loops don't seem to run as the length of the object when console logging it is 0.
let temp = [];
db.collection("Users").onSnapshot(res => {
const changes = res.docChanges();
changes.forEach(change => {
if (change.type === "added") {
temp.push({
id: change.doc.id,
email: change.doc.data().email
});
}
});
});
console.log(temp);
console.log(temp.length);
I expected the 2nd console log to be 2 but it outputs 0. The weird thing is when I look at the object from the console log above, it shows it has a length of 2 and shows the current data in it:
Data is loaded from Firestore asynchronously. Since this may take some time, your main code will continue to run while the data is loading. Then when the data is loaded, your callback functions is called.
You can easily see this in practice by adding some logging:
console.log("Before starting onSnapshot");
db.collection("Users").onSnapshot(res => {
console.log("Got data");
});
console.log("After starting onSnapshot");
When you run this code, it logs:
Before starting onSnapshot
After starting onSnapshot
Got data
This is probably not the order you expected, but it completely explains why the log statements don't work as you expected. By the time you log the array length, the data hasn't been loaded from the database yet.
The reason logging the array does seem to work, is that Chrome updates the log output after the data has loaded. If you change it to console.log(JSON.stringify(temp));, you'll see that it logs an empty array.
This means that all code that needs access to the data, must be inside the callback, or be called from there:
let temp = [];
db.collection("Users").onSnapshot(res => {
const changes = res.docChanges();
changes.forEach(change => {
if (change.type === "added") {
temp.push({
id: change.doc.id,
email: change.doc.data().email
});
}
});
console.log(temp);
console.log(temp.length);
});
As you can probably imagine, many developers who are new to asynchronous APIs run into this problem. I recommend studying some of the previous questions on this topic, such as:
How do I return the response from an asynchronous call?
THEN ARRAY EMPTY PROMISSE
How to get data from firestore DB in outside of onSnapshot
Related
This question already has answers here:
Using async/await with a forEach loop
(33 answers)
how to do async await on a forEach function [duplicate]
(2 answers)
Closed last month.
I know similar questions have been asked here:
I need to pull data from another Firestore collection based on forEach data within a onSnapshot call
and here:
Using getDoc().then() inside of a loop Firebase
but I really cannot adapt it to my own issue. I actually also consulted ChatGPT, but apparently, it only knows firebase v8.x.x :/
I have a comment collection and I'm querying these for specific content (url). I can get the comments in the first step just fine. Each comment document has a field called "user_id", I want to use this user ID to get information about the user from the collection "userinfo" and push it to the comments array. From what I can understand, the promise I get in my attempt (see code below), indicates that I need to use await, but this is apparently tricky in for loops and inside useEffect.
My attempt is below:
const [allCommments, setAllCommments] = useState([]);
const commentRef = collection(db, "comments");
const q = query(commentRef, where("content", "==", url));
useEffect(() => {
onSnapshot(q, (snapshot) => {
const tempComments = [];
snapshot.forEach((snapDoc) => {
tempComments.push(snapDoc); // this works fine, the next steps doesn't:
const userSnap = getDoc(doc(db, "userinfo", snapDoc.data().user_id));
// tempComments.push(userSnap.data().firstname).push(userSnap.data().lastname) // what I want
// console.log(userSnap.data()) // this gives the error "userSnap.data() is not a function"
// console.log(userSnap); // this only returns a promise
});
setAllCommments(tempComments);
});
}, []);
Im trying to assign variables to their respected value from the firestore database using the get doc function, I've noticed it does not assign or update the values what so ever.
I've tried to work with async and awaits but cannot seem to make it work.
getFromDatabase(nameOfCollection,nameOfDocument){
const db = firebase.firestore();
var docRef = db.collection(nameOfCollection).doc(nameOfDocument);
docRef.get().then(function(doc) {
if (doc.exists) {
outvariable = doc.data().anyfield; // THIS IS WHAT I WANT
console.log(" Document data:", doc.data());
} else {
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
}
im expecting outvariable = doc.data().anyfield
Most likely you're confused by the fact that data is loaded from Firestore asynchronously. It's not so much that the data isn't assigned to the values, because it really is. It just happens at a different time than you expect.
It's easiest to see this by adding some simple logging statements around the code that loads data:
const db = firebase.firestore();
var docRef = db.collection(nameOfCollection).doc(nameOfDocument);
console.log("Before starting to load data");
docRef.get().then(function(doc) {
console.log("Got data";
});
console.log("After starting to load data");
When you run this code, the output is:
Before starting to load data
After starting to load data
Got data
This is probably not what you expected, but it's actually completely correct. The data is loaded from Firestore asynchronously (since it may take some time), and instead of waiting, the main code continues. Then when the data is available, your callback function is called with that data.
This means that any code that requires the data from the database must be inside the callback, or be called from there. So the console.log(" Document data:", doc.data()) in your original code should work fine. But a similar console.log outside of the callback won't work, because it runs before the data is available.
This is an extremely common source of confusion for developers new to this type of API. But since most modern web/cloud APIs, and many other APIs, are asynchronous, it's best to learn how to work with them quickly. For that, I recommend reading:
get asynchronous value from firebase firestore reference
Doug's blog post on why Firebase APIs are synchronous
Firestore query in function return
NodeJS, Firestore get field
How do I return the response from an asynchronous call?
The data can be extracted with .data() or .get() to get a specific field.
For example: doc.get(anyfield);
More info can be found on the official documentation.
My code below attempts to insert a Timestamp into Firebase. The Timestamp I'm referring to is this one here. I plan to retrieve the inserted Timestamp and convert said Timestamp to a Date JS Object to be displayed when a user requests for said time. I tried retrieving said Timestamp and encountered something rather strange to me.
I have the following code, initially:
let newRef = firebase.database().ref().child('fs').push(); //'fs' here is one of the nodes in my Firebase
newRef.set({
//Set some other data
timeCreated: firebase.database.ServerValue.TIMESTAMP
}).then(() => {
console.log('Insert successful');
});
newRef.once('value').then((snapshot) => {
console.log(snapshot.val().timeCreated);
console.log(new Date(snapshot.val().timeCreated));
});
I soon noticed that I was breaking my chain of Promise. Because of that, I changed it into the following:
let newRef = firebase.database().ref().child('fs').push();
newRef.set({
//Some other data
timeCreated: firebase.database.ServerValue.TIMESTAMP;
}).then(() => {
console.log('Insert successful');
return newRef.once('value').then((snapshot) => snapshot.val().timeCreated);
}).then((timeRetrieved) => {
console.log(timeRetrieved);
console.log(new Date(timeRetrieved);
});
And it retrieved the correct data.
My confusion is at the initial code. Why does Firebase still somehow successfully retrieved timeCreated when the process of set() hasn't been completed? I looked at the console.log(...) and the first one to appear is the reading data process. However, as Firebase hasn't even set the value of timeCreated, shouldn't I be reading a null or some other null-like value? Instead, the reading data code returned a Timestamp that is slightly smaller in value to the Timestamp that is to be set().
EDIT
As per request, these are the results I get using my initial code:
However, the value of timeCreated stored in my Firebase is: 1556862732784, which is different from the result of the image above (the result of the image above is the result of console.log(snapshot.val().timeCreated) [1556862732273]).
When you execute this code:
let newRef = firebase.database().ref().child('fs').push(); //'fs' here is one of the nodes in my Firebase
newRef.set({
//Set some other data
timeCreated: firebase.database.ServerValue.TIMESTAMP
}).then(() => {
console.log('Insert successful');
});
newRef.once('value').then((snapshot) => {
console.log(new Date(snapshot.val().timeCreated));
});
You will get the execution of logs in the following order:
console.log(new Date(snapshot.val().timeCreated));
console.log('Insert successful');
and that's because set() is asynchronous, so the code after it will be executed before the data is even retrieved.
Instead, the reading data code returned a Timestamp that is slightly smaller in value to the Timestamp that is to be set().
Since you created a new Date() in the console.log(), then you probably got the following time.
console.log(new Date(null));
I'm learning React and Firestore currently and am a bit stuck. I'm trying to retrieve a users name from a firestore collection by searching their uid.
The following code is executed in a map of 'lessons' to create a list.
{lesson.post_author && findName(lesson.post_author)}
The following code is the findName function.
let findName = uid => {
firebase.firestore().collection("users")
.where('uid', '==', uid)
.get()
.then(querySnapshot => {
console.log(querySnapshot.docs[0].data().name);
});
};
Currently, the findName function will console log all of the names to the console successfully. I've altered the code to be able to console log outside of the firestore call, but that returns a promise pending in console.
The goal of the code is to return the name rather then the uid in the list.
Any help would be much appreciated.
Thank you!
As others have explained, you can't return that value, since it's loaded from Firestore asynchronously. By the time your return runs, the data hasn't loaded yet.
In React you handle this by putting the data in the component's state, and using it from there. If you do this, your render method can simply pick it up from the state, with something like:
{lesson.post_author && findName(lesson.post_author_name)}
(the above assumes that lesson indirectly comes from the state.
It's a bit easier if we pretend there's only one lesson, and you have these values straight in the state:
{state.post_author && findName(state.post_author_name)}
Now I'll assume you already have the post_author and you just need to look up the author's name. That means that somewhere in/after componentDidMount you'll load the additional data and add it to the state:
componentDidMount() {
firebase.firestore().collection("users")
.where('uid', '==', this.state.uid)
.get()
.then(querySnapshot => {
this.setState({ post_user_name: querySnapshot.docs[0].data().name });
});
}
Now the loading of the data still happens asynchronously, so the call to setState() happens some time after componentDidMount has completed. But React is aware that changing the state may require a refresh of the component, so it responds to the call to setState() by rerendering it.
Note that I'd highly recommend using each user's UID as the ID of the documents in users. That way you don't need a query and can just do a directly lookup:
componentDidMount() {
firebase.firestore().collection("users")
.doc(this.state.uid)
.get()
.then(doc => {
this.setState({ post_user_name: doc.data().name });
});
}
I'm trying to retrieve a users name from a firestore collection by
searching their uid.
This is accomplished by using the asyncronous .get method on a Firestore reference. In your case, you probably have a users collection of firebase.auth().currentUser.uid named documents.
var userRef = firebase.firestore().collection('users').doc(users.uid);
userRef.get().then(function(doc) {
if (doc.exists) {
console.log("Users first name is:", doc.data().firstName);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
Firebase provides samples on GitHub for writing cloud functions.
I have a question about the "createStripeCharge" function.
It is possible that the write to the database fails?
If this would be the case, this function charges a customer but no object would be saved to the database. Is this right?
This error handling cannot be enough or do I understand something wrong?
The following line in the code confuses me:
return event.data.adminRef.set(response);
You found the code on GitHub:
https://github.com/firebase/functions-samples/blob/master/stripe/functions/index.js
Or here:
exports.createStripeCharge = functions.database.ref('/stripe_customers/{userId}/charges/{id}').onWrite((event) => {
const val = event.data.val();
// This onWrite will trigger whenever anything is written to the path, so
// noop if the charge was deleted, errored out, or the Stripe API returned a result (id exists)
if (val === null || val.id || val.error) return null;
// Look up the Stripe customer id written in createStripeCustomer
return admin.database().ref(`/stripe_customers/${event.params.userId}/customer_id`).once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
// Create a charge using the pushId as the idempotency key, protecting against double charges
const amount = val.amount;
const idempotency_key = event.params.id;
let charge = {amount, currency, customer};
if (val.source !== null) charge.source = val.source;
return stripe.charges.create(charge, {idempotency_key});
}).then((response) => {
// If the result is successful, write it back to the database
return event.data.adminRef.set(response);
}).catch((error) => {
// We want to capture errors and render them in a user-friendly way, while
// still logging an exception with Stackdriver
return event.data.adminRef.child('error').set(userFacingMessage(error));
}).then(() => {
return reportError(error, {user: event.params.userId});
});
});
The only way a database write can fail is if it violates a security rule or if the function times out and gets cleaned up before it finishes. When using the admin SDK, security rules don't apply, so that can't be the cause. I suppose it's possible for the write to time out, so if you are very concerned about that, you should increase the timeout of the function. The chance of a timeout occurring is extremely low. If that happens, you should be able to manually resolve the concern of an end user without wasting a lot of time.
The onWrite():
triggers when data is created, updated, or deleted in the Realtime Database.
exports.createStripeCharge = functions.database.ref('/stripe_customers/{userId}/charges/{id}').onWrite((event) => {
Therefore if the write to the database in the above location fails then onWrite() will not trigger.
Also if you want it to trigger when adding data only then you can use onCreate():
It triggers when new data is created in the Realtime Database.
more info here:
https://firebase.google.com/docs/functions/database-events