I am trying to export a firestore function that performs a query and returns an array containing the objects in that query. I am trying to get data from a subcollection of a document, and get an array of document objects returned to render to the client.
I've tried the below but it's not working (e.g. the object returns blank). I think this has to do with improper handling of promises, but couldn't figure it out on my own. Thanks for your help.
export const getEvents = (id) => {
let events = [];
firestore.collection('users')
.doc(id)
.collection('events')
.get()
.then((snapshot) => {
snapshot.forEach((doc) => events.push(doc));
});
return events;
};
You are correct in identifying this problem is related to the handling of promises. You are returning the events array before it has a chance to be populated, because the promise hasn't resolved yet.
If your environment allows it, I would recommend that you use async/await, because it makes the code much easier to read and understand, like this:
export const getEvents = async (id) => {
let events = [];
const snapshot = await firestore.collection('users')
.doc(id)
.collection('events')
.get()
snapshot.forEach((doc) => events.push(doc));
return events;
};
But if you can't use async/await you can do it with promises. But you need to only resolve the promise after the data is fetched:
const getEvents = (id) => {
return new Promise((resolve, reject) => {
let events = [];
const snapshot = firestore.collection('users')
.doc(id)
.collection('events')
.get()
.then((snapshot) => {
snapshot.forEach((doc) => events.push(doc));
resolve(events); // return the events only after they are fetched
})
.catch(error => {
reject(error);
});
});
};
Related
I'm trying to build compound query in Expo react native - firestore.
I have 2 collections in firebase. First "node" is userID and second are IDs of places that had been discovered by this user. Then, I need to take this array of place IDs and pass it as parameter in 2nd query where I got name of each place stored in collection named "databaseOfPlaces". (I want to make scrollable view with names, so maybe I should add listener later on?)
My solution is not working very well. Can you help me? Is this the right way, or is there another way how to save DB call?
Thank you very much.
This is my code:
async componentDidMount() {
db.collection("placesExploredByUsers") // default
.doc("mUJYkbcbK6OPrlNuEPzK") // default
.collection(auth.currentUser.uid)
.get()
.then((snapshot) => {
if (snapshot.empty) {
alert("No matching documents.");
return;
}
const users = [];
snapshot.forEach((doc) => {
const data = doc.data();
users.push(data);
});
this.setState({ users: users });
})
.catch((error) => alert(error));
db.collection("databaseOfPlaces")
.where('placeID','in',this.state.users)
.get()
.then((snapshot) => {
if (snapshot.empty) {
alert("No matching documents.");
return;
}
const places = [];
snapshot.forEach((doc) => {
const data = doc.data();
places.push(data);
});
this.setState({ places: places });
})
.catch((error) => alert(error));
}
Data is loaded from Firestore (and most modern cloud APIs) asynchronously. By the time your second query now runs, the results for the first query are not available yet.
Because of this, any code that needs the results from the first query, will need to be inside the then() callback of that query.
So:
async componentDidMount() {
db.collection("placesExploredByUsers") // default
.doc("mUJYkbcbK6OPrlNuEPzK") // default
.collection(auth.currentUser.uid)
.get()
.then((snapshot) => {
if (snapshot.empty) {
alert("No matching documents.");
return;
}
const users = [];
snapshot.forEach((doc) => {
const data = doc.data();
users.push(data);
});
this.setState({ users: users });
db.collection("databaseOfPlaces")
.where('placeID','in', users)
.get()
.then((snapshot) => {
if (snapshot.empty) {
alert("No matching documents.");
return;
}
const places = [];
snapshot.forEach((doc) => {
const data = doc.data();
places.push(data);
});
this.setState({ places: places });
})
})
.catch((error) => alert(error));
}
I've made a simple class that returns the data downloaded from firebase. The issue is that if I console.log data in the class, it gives data as expected. However, if I import this class anywhere else and try to use it, it returns data as undefined.
Can you explain what's wrong?
My getCollection func in class dbAPI (data is correct)
getCollection(collection) {
dataBase
.collection(collection)
.get()
.then(querySnapshot => {
let data = querySnapshot.docs.map(doc => doc.data())
console.log(data)
return data
})
.catch(function(error) {})
}
The way I try to get data (data is undefined here)
componentDidMount() {
const db = new dbAPI()
let data = db.getCollection("collectionName")
this.setState({ data })}
The issue is that you're trying to return data from a callback to a synchronous function, which is impossible (link). You need to either promisify your getCollection function, or use async/await. If you want to convert your code to use async/await, check out this link. I put an example below. Basically, you need to await for the get request to get the data, then perform your transformation. After performing that transformation, you can return data.
async getCollection(collection) {
const collectionRef = dataBase.collection(collection);
try {
const dataSnapshot = await collectionRef.get();
const data = querySnapshot.docs.map(doc => doc.data());
console.log(data);
return data;
} catch (err) {
console.log(err);
}
}
In your componentDidMount, you must add async/await keywords to the appropriate functions. Async needs to go to the top to mark componentDidMount as async, and you need to await the data from the function call db.getCollection.
async componentDidMount() {
const db = new dbAPI()
const data = await db.getCollection("collectionName")
this.setState({ data })
}
Thanks to you I've managed to do it, really appreaciate your help, my solution:
getCollection:
async getCollection(collection) {
let data = null
await dataBase
.collection(collection)
.get()
.then(querySnapshot => {
data = querySnapshot.docs.map(doc => doc.data())
})
.catch(function(error) {
console.error(`Data fetch failed: \n${error}`)
data = "ERROR"
})
return data}
getting data:
async componentDidMount() {
const db = new Database()
const data = await db.getCollection("collectionName")
this.setState({ ...data })}
I am trying to grab data from firebase and it console logs correctly but array says length is 0
useEffect(() => {
let items = [];
const unsubscribe = store
.collection('users')
.doc(user.uid)
.collection('redirects')
.onSnapshot(snapShot => {
snapShot.forEach(getPath => {
const { path } = getPath.data();
store.doc(path).onSnapshot(doc => {
const data = doc.data();
items.push({ ...data });
});
});
});
setDocs(items);
setIsLoading(false);
return () => unsubscribe();
}, []);
console.log(docs);
Try something like this:
Your setState call should be inside the onSnapShot(()=>{}) callback, because the it's asynchronous. The way you're doing, you're basically trying to call setDocs() with an empty items array.
useEffect(() => {
let items = [];
const unsubscribe = store
.collection('users')
.doc(user.uid)
.collection('redirects')
.onSnapshot(snapShot => {
snapShot.forEach(getPath => {
const { path } = getPath.data();
store.doc(path).onSnapshot(doc => {
const data = doc.data();
items.push({ ...data });
});
});
setDocs(items);
setIsLoading(false);
});
//setDocs(items);
//setIsLoading(false);
return () => unsubscribe();
}, []);
As others have mentioned, it seems to be a async issue.
The console in chrome will show resolved object values, not necessarily what they were at run time.
To show you what i mean, change your console log from console.log(docs) to console.log(JSON.stringify(docs)) and it will show you the string value of docs at run time which should be an empty array, which would make the length 0 make sense (because it is 0 until the async call is resolved).
I'm using react-native and I want to get data from the firebase realtime database and set state of that data and then only then load the view, I don't want the user to see the data getting pushed and mapped on every load of the chat view.
Here is what I've tried
_getMessages = async () => {
let message_array = [];
await firebase.database().ref('User-Message').child(this.state.fromUser).child(this.state.toUser).on('child_added', async (snapshot) => {
let message_id = await snapshot.key;
let message_ref = await firebase.database().ref('Message').child(message_id).once('value', async (payload) => {
await message_array.push(payload.val())
})
await this.setState({ messages : message_array })
})
}
And in my componentWillMount is simply call the _getMessages() function like this
componentWillMount = async () => {
await this._getMessages();
}
How can I make sure to set the state of messages after getting all the messages from the firebase?
This won't work:
await firebase.database().ref('User-Message').child(this.state.fromUser).child(this.state.toUser).on('child_added', async (snapshot) => {
Firebase's on() method starts actively listening for events. It does not have a clear moment when it's done, so doesn't return a promise. Hence it can't be used with await/async.
My feeling is that you're trying to simply load all user messages, which is easiest to do by using once("value":
let ref = firebase.database().ref('User-Message').child(this.state.fromUser).child(this.state.toUser);
let message_array = await ref.once('value', async (snapshot) => {
let messagePromises = [];
snapshot.forEach(function(userMessageSnapshot) {
messagePromises.push( firebase.database().ref('Message').child(snapshot.key).once('value', async (payload) => {
return payload.val();
})
})
await messageSnapshots = Promise.all(messagePromises);
this.setState({ messages : messageSnapshots })
})
If you want to get realtime updates, you will have to use an on() listener. But that does mean you can't use async/await in the outer listener. It'd look something like this:
let ref = firebase.database().ref('User-Message').child(this.state.fromUser).child(this.state.toUser);
ref.on('value', async (snapshot) => {
let messagePromises = [];
snapshot.forEach(function(userMessageSnapshot) {
messagePromises.push( firebase.database().ref('Message').child(snapshot.key).once('value', async (payload) => {
return payload.val();
})
})
await messageSnapshots = Promise.all(messagePromises);
this.setState({ messages : messageSnapshots })
})
Firebase Firestore Guides show how to iterate documents in a collection snapshot with forEach:
db.collection("cities").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log(doc.id, " => ", doc.data());
});
});
I imagined it would support map as well, but it doesn't. How can I map the snapshot?
The answer is:
querySnapshot.docs.map(function(doc) {
# do something
})
The Reference page for Firestore reveals the docs property on the snapshot.
docs non-null Array of non-null firebase.firestore.DocumentSnapshot
An array of all the documents in the QuerySnapshot.
Got pretty sick and tired of Firestore returning stuff in their classes or whatever. Here's a helper that if you give it a db and collection it will return all the records in that collection as a promise that resolves an actual array.
const docsArr = (db, collection) => {
return db
.collection(collection)
.get()
.then(snapshot => snapshot.docs.map(x => x.data()))
}
;(async () => {
const arr = await docsArr(myDb, myCollection)
console.log(arr)
})()
// https://firebase.google.com/docs/firestore/query-data/get-data
const querySnapshot = await db.collection("students").get();
// https://firebase.google.com/docs/reference/js/firebase.firestore.QuerySnapshot?authuser=0#docs
querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
Here's another example
var favEventIds = ["abc", "123"];
const modifiedEvents = eventListSnapshot.docs.map(function (doc) {
const eventData = doc.data()
eventData.id = doc.id
eventData.is_favorite = favEventIds.includes(doc.id)
return eventData
})
I have found that a better way to do this by using map and get your document id as well is as follows:
start with the object array I wish to update in your constructor:
this.state = {
allmystuffData: [
{id: null,LO_Name: "name", LO_Birthday: {seconds: 0, nanoseconds: 0},
LO_Gender: "Gender", LO_Avatar: "https://someimage", LO_Type: "xxxxx"},],
};
and in my function do the following
const profile = firebase
.firestore()
.collection("users")
.doc(user.uid)
.collection("stuff")
.get()
.then( async (querySnapshot) => {
console.log("number of stuff records for ",user.uid," record count is: ",
querySnapshot.size);
const profile = await Promise.all(querySnapshot.docs.map( async (doc) => {
const stuffData = doc.data()
stuffData.id = doc.id
condole.log("doc.id => ",doc.id)
return stuffData
}));
this.setState({allmystuffData: profile});
})
.catch(function (error) {
console.log("error getting stuff: ", error);
})
In this example I read all the documents in the collection with querysnapshot the when mapping accross them. the promise.all ensures that all the records are returned before you render it to your screen. I add the document id to the "id" element of each object in the array returned, then I use setstate to replace my state array with the array returned from the query.
you can try this
FirebaseFirestore.instance
.collection('Video_Requests')
.get()
.then((QuerySnapshot querySnapshot){querySnapshot.docs.forEach((doc){
print(doc.data());
});
});