I have a Firestore listener to grab chat messages for which I use a limit of 10. Everything works well on first load. However, when there are 10 messages and the limit is reached, the chat does not accept new messages. It is as if the limit function accepts the first 10 messages and then will not take any more when I need it to be updated with every new message. Here's the listener code:
startChat() {
document.getElementById("myForm").style.display = "block";
const ref = firebase
.firestore()
.collection("Chats")
.doc(this.state.uid)
.collection("Messages");
const query = ref.orderBy("timestamp", "asc").limit(10);
this.unsubFromMessages = query.onSnapshot(
(snapshot) => {
console.log(
snapshot.docs.map((doc) => {
return doc.data();
})
);
if (snapshot.empty) {
console.log("No matching documents.");
firebase
.firestore()
.collection("Chats")
.doc(this.state.uid)
.set({
name: this.state.displayName,
uid: this.state.uid,
email: this.state.email,
})
.then(console.log("info saved"))
.catch((error) => {
console.log("Error saving info to document: ", error);
});
}
snapshot.docChanges().forEach((change) => {
if (change.type === "removed") {
console.log(change.doc.data().content);
} else if (change.type === "added") {
this.setState((state) => {
const messages = [
...state.messages,
{ id: change.doc.id, body: change.doc.data() },
];
return {
messages,
};
});
setTimeout(this.scrollToBottom(), 2000);
}
});
},
(error) => {
console.log(error);
}
);
}
Does anyone know why this is happening and how to make the limit function accept new messages? Thanks.
orderBy("timestamp", "asc")
With this order, you will be getting back the 10 oldest messages. New messages will have a higher timestamp, and so a new 11th message will not be part of the oldest 10.
If instead you want the 10 newest messages, use descending order:
orderBy("timestamp", "dsc")
Nicholas's answer does, indeed, solve the issue. However, it does not solve the issue of messages loading in the wrong order when the chat is closed, then started and the previously entered messages are shown. One way to solve this issue is adding reverse() to docChanges() like this: snapshot.docChanges().reverse().forEach. Hope this might help someone. Happy coding, everyone. Stackoverflow rocks!
Related
if (currentMessage !== "") {
const messageData = {
room: room,
author: username,
message: currentMessage,
time:
new Date(Date.now()).getHours() +
":" +
new Date(Date.now()).getMinutes(),
};
await socket.emit("send_message", messageData);
setMessageList((list) => [...list, messageData]);
setCurrentMessage("");
}
};
useEffect(() => {
socket.on("receive_message", (data) => {
setMessageList((list) => [...list, data]);
});
}, [socket]);
In the output, I am receiving one message twice in the receiver side.
I have figured this has to be related to useEffect Hook.
Please help.
I was having Strict Mode enabled in React
that was causing the problem.
Now its working fine.
I am trying to get all the documents from a Firestore collection. I am following their docs to do so. However, nothing happens. I do not see anything console-logging or any errors whatsoever. Basically, nothing happens, as if the get() function is not working. I am clueless as to what is going on.
Here's the code with the get(). It is written in ReactJS with hooks:
import React, { useState, useEffect } from 'react';
import firebase from '../firebase.js';
import './Messages.css';
function Messages (props) {
const [messages, setMessages] = useState([]);
const [customers, setCustomers] = useState([]);
useEffect(() => {
const query = firebase
.firestore()
.collection("Chats")
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
if (doc.exists) {
console.log(doc)
} else {
console.log('nothing')
}
console.log(doc.id, " => ", doc.data());
});
}).catch((error) => {
console.log("Error getting documents: ", error);
});
}, []);
Similarly, the same thing was happening to the onSnapShot function that I tried to use as follows:
useEffect(() => {
const unsub = query.onSnapshot(async querySnapshot => {
if (querySnapshot.docChanges().length > 0) {
const data = querySnapshot.docs.map((doc) => {
return { body: doc.data(), id: doc.id }
}
);
const allData = await Promise.all(data)
setCustomers(allData);
} else { console.log("no data") }
}, (error) => {
console.log("Error listening to documents: ", error);
})
return () => {
unsub();
};
}, []);
The Customers state is empty and console-logging from anywhere within Querysnapshot function does not work. Strangely, however, similar set-ups/codes work in my other components. Adding dependencies or placing the code into another function (fetchData()) and calling it within useEffect did not work. I am also certain it is not an issue with Rules as I am accessing Firestore as an admin for which I have an appropriate Rule set up (match /{documents=**} { allow write, read: if request.auth.token.admin == true; }). I am clueless as to what is going on. Please help.
P.S. The following code does work within the useEffect in the same component, however, but since I can not figure out how to get rid of duplicates that I get from the query in the customers state (tried array.includes and indexof but failed) as I kept getting only one same object, I am ditching the following code in favor of one of the above.
const query = firebase.firestore()
.collectionGroup('Messages')
.orderBy("timestamp", "desc")
const unsub = query.onSnapshot((snapshot) => {
if (snapshot.empty) {
console.log('No matching documents.');
return;
}
snapshot.docs.map((doc) => {
console.log("1")
if (customers.length ) {
console.log("1b")
customers.map((customer) => {
console.log("2")
if (customer.uid.indexOf(doc.data().uid) === -1) {
console.log("3")
setCustomers(prevFiles => ([...prevFiles, {
name: doc.data().name, uid: doc.data().uid
}]))
}
})
} else {
console.log("4")
setCustomers([{name: doc.data().name, uid: doc.data().uid}])
}
});
}, (error) => {
console.log('Error getting messages', error);
})
I figured it out. This might be very useful for somebody who is in a similar situation to know as this does not seem to be explained in the Firestore docs.
Anyways, the reason nothing was happening and console.logging was not working was because the query was empty. So, make sure to always check that your collection has documents which in turn have some information. The documents that I was trying to get contained a collection and named after user ids which I needed. However, since they did not really contain any information, they could not be retrieved. And, I did not know that. I simply needed the names of the documents, the uids.
One way you can check if your snapshot is empty is as follows:
if (snapshot.empty) {
console.log('No matching documents.');
return;
}
In my code, it looks like this:
const query = firebase
.firestore()
.collection("Chats")
.get()
.then((querySnapshot) => {
if (querySnapshot.empty) {
console.log('No matching documents.');
return;
}
querySnapshot.forEach((doc) => {
if (doc.exists) {
console.log(doc)
setCustomers(prevFiles => ([...prevFiles, {
id: doc.data().id, data: doc.data()
}]))
} else {
console.log('nothing')
}
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
}).catch((error) => {
console.log("Error getting documents: ", error);
});
Basically, it will never get to the forEach part if it is empty.
Hope this sheds some light on some confusion that someone might have about the docs in collections. They must contain info to be retrievable. Happy coding.
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));
}
Im using the firebase admin SDK server-side, and in one of my routes, im getting a collection, and then looping over each document in said collection...during each iteration, i use a property value to do a second get(). in the then() of this secondary get(), i again use data from the initial get to make final, tertiary get().
unfortunately the asynchronous nature of these nested calls seems to be creating undesirable outcomes.
heres the route function:
router.get('/list', authorization, (req, res) => {
console.log('/reports/list entered...')
admin
.firestore()
.collection('user-reports')
.get()
.then(querySnapshot => {
const reports = []
querySnapshot.forEach(snapshotDocument => {
const closed = snapshotDocument.get('closed')
console.log(`closed status: ${closed}`)
if (closed === false) {
const data = snapshotDocument.data()
console.log(`condition passed, data: ${JSON.stringify(data)}`)
// get # of reports made by sender
admin
.firestore()
.collection('users')
.doc(data.reportee)
.get()
.then(doc => {
data['reportee'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`first then => data; ${JSON.stringify(data)}`)
// get report history of reportee
admin
.firestore()
.collection('users')
.doc(data.reporter)
.get()
.then(doc => {
data['reporter'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`second then, ${JSON.stringify(data)}`)
reports.push(data)
})
.catch(err => {
return res.json({ error: true, message: err })
})
})
.catch(err => {
return res.json({ error: true, message: err })
})
}
})
console.log(`pre-response: ${JSON.stringify(reports)}`)
return res.json({ reports })
})
.catch(err => res.json({ error: true, message: err }))
})
what im logging is the "first condition passed", "pre-response: []", and "first then => data". by the time i ultimately return "reports" its empty. is there a more effective way to run firestore methods inside of foreach loops?
The solution almost always is to use Promise.all() to wait for a bunch of promises to resolve:
rturn Promise.all(querySnapshot.map(snapshotDocument => {
const closed = snapshotDocument.get('closed')
console.log(`closed status: ${closed}`)
if (closed === false) {
const data = snapshotDocument.data()
console.log(`condition passed, data: ${JSON.stringify(data)}`)
// get # of reports made by sender
return admin
.firestore()
.collection('users')
.doc(data.reportee)
.get()
.then(doc => {
data['reportee'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`first then => data; ${JSON.stringify(data)}`)
// get report history of reportee
return admin
.firestore()
.collection('users')
.doc(data.reporter)
.get()
.then(doc => {
data['reporter'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`second then, ${JSON.stringify(data)}`)
return data;
})
.catch(err => {
return res.json({ error: true, message: err })
})
})
.catch(err => {
return res.json({ error: true, message: err })
})
}
})
.then((reports) => {
console.log(`pre-response: ${JSON.stringify(reports)}`)
return res.json({ reports })
})
There might be some syntax errors, but the basic changes:
Turn the documents into a list of promises/results with querySnapshot.map(snapshotDocument => {.
Use Promise.all to create a promise that resolves when all these results are done.
Returns results from as deep down as needed all the way up to the main code.
Note that nesting of promises the way you do here is unusual and an anti-pattern, so in recommend also looking into learning to chain promises (so that you end up with a single catch - instead of having them all over the place.
I am having trouble with firebase and expo. When we do the signup process, and the user is generated through firebase, we sometimes get "undefined is not an object (evaluating 0.docs[0].data)" and the user is redirected to the entry point of the application instead of going to the next screen.
Most of the users will be able to go through the process without any problem. But few reported the same issues, and I have seen that with one account.
Here below the function that does not work properly
const createUser = async () => {
//Check if user already exists
await props.firebase.db
.collection('users')
.where('email', '==', props.userProfile.email)
.get()
.then(async (snapShot) => {
//if no document is found, save user to db
if (snapShot.docs.length === 0) {
await saveUserToDataBase()
.then(() => {
setShowLoader(false);
props.navigation.navigate('NotificationPermission');
})
.catch((err) => Alert.alert('An error occurred', err.message));
}
//else log an error, TODO: add error text in app.
else {
setShowLoader(false);
}
})
.catch((err) => {
Alert.alert('An error occurred', err.message);
});
};
const saveUserToDataBase = async () => {
//finds the correct collection and creates a new document within in containing the profile data.
await props.firebase.db
.collection('users')
.add({
first_name: props.userProfile.firstName,
email: props.userProfile.email.toLowerCase(),
phone_number: props.userProfile.phoneNumber,
organization: props.userProfile.organization,
profileId: props.userProfile.profileId,
})
.then(async (docRef) => {
await props.firebase.db
.collection('profile')
.doc(props.userProfile.profileId)
.update({
user_id: docRef.id,
})
.then(async () => {
await uploadProfilePhoto(docRef.id);
if (props.accessToken) {
props.navigation.navigate('NotificationPermission');
} else {
props.navigation.navigate('NotificationPermission');
}
})
.catch((err) => {
Alert.alert(
'An error occurred updating the users profile ',
err.message,
);
});
})
.catch((err) => {
Alert.alert('An error occurred creating the user', err.message);
});
};
I have used the Alert component to check the error directly, the error is consistent with few email addresses. I have upgraded the firebase package, did no do anything.
I feel like the onAuthStateChanged could be responsible of that but I am not sure how to handle that? Your help or suggestions will be greatly appreciated.