Why is Firestore rejecting the operation? - firebase

I have a firestore action in a react-native project:
export const getMessages = (convoId) => {
return (dispatch) => {
firebase
.firestore()
.collection('messages')
.where('userConvos', 'array-contains', convoId)
.orderBy('time')
.onSnapshot((querySnapshot) => {
const messages = [];
querySnapshot.forEach((doc) => {
messages.push(doc.data());
});
dispatch({ type: types.LOAD_MSGS, payload: messages });
});
};
};
the two composite indices I've tried in firestore:
and my database structure is like so:
Messages (collection)
content (field)
time (field)
userConvos (field)
The problem is when I make this firestore call, I get the
Error: Firestore: Operation was rejected because the system is not in a state required for the opereation's execution
But if the error is dismissed, the data is ordered as expected. What's going on and how can this error be eliminated?

You have a missing index. To create it, do the following.
View the android device logs by typing on the terminal:
adb logcat
If you reproduce the error, then something like the following will appear in the logs:
05-16 04:23:20.887 3746 3895 I ReactNativeJS: nativeErrorMessage:
'FAILED_PRECONDITION: The query requires an index. You can create it
here:
https://console.firebase.google.com/v1/r/project/XXXXXXXXX/firestore/indexes?create_composite=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
Open a browser and paste the link.
Press the Create Index button.

Related

Firebase listener downloads data after leaving and getting back to screen

I implemented a chatscreen inside my app and the following code represents the important sample of the code and I noticed that something about the data usage is very odd. The code is a little bit longer code sample but I will explain it after that.
const CountryChat = props =>{
var chosenLanguage = useSelector(state => state.myLanguage.myLanguage);
const countryId = props.navigation.getParam("countryId");//already upper case so no worries about correct firestore adress
const countryName = props.navigation.getParam("countryName");
const userId = useSelector(state => state.auth.userId);
const [TItext, setTItext] = useState("");
const [chatmessages, setChatMessages] = useState(() => []);//dummydata so FlatList wont crash because messages are empty during first renderprocess
const [refreshFlatlist, setRefreshFlatList] = useState(false);
const [myProfilePic, setMyProfilePic] = useState(null);
useEffect(() => {
downloadProfilePic();
var loadnewmessages = firebase.firestore().collection("group_rooms").doc("group_rooms").collection(`${countryId}`).orderBy("timestamp").limit(30).onSnapshot((snapshot) => {
var newmessages = [];
var deletedmesssages = [];
snapshot.docChanges().forEach((change) => {
if(change.type === "added"){
newmessages.push({
counter: change.doc.data().counter,
sender: change.doc.data().sender,
timestamp: change.doc.data().timestamp.toString(),
value: change.doc.data().value,
displayedTime: new Date(change.doc.data().displayedTime),
senderProfilePic: change.doc.data().senderProfilePic
})
};
if(change.type === "removed"){
deletedmesssages.push({
counter: change.doc.data().counter,
sender: change.doc.data().sender,
timestamp: change.doc.data().timestamp.toString(),
value: change.doc.data().value,
displayedTime: new Date(change.doc.data().displayedTime),
senderProfilePic: change.doc.data().senderProfilePic
})
};
})
if(newmessages.length > 0){
setChatMessages(chatmessages => {
return chatmessages.concat(newmessages)
});
};
if(deletedmesssages.length > 0){
setChatMessages(chatmessages => {
var modifythisarray = chatmessages;
let index = chatmessages.map(e => e.timestamp).indexOf(`${deletedmesssages[0].timestamp}`);
let pasttime = Date.now() - parseInt(modifythisarray[index].timestamp);
modifythisarray.splice(index, 1);
if(pasttime > 300000){
return chatmessages
}else{
return modifythisarray
}
});
setRefreshFlatList(refreshFlatlist => {
//console.log("Aktueller Status von refresher: ", refreshFlatlist);
return !refreshFlatlist
});
}
newmessages = [];
deletedmesssages = [];
});
return () => { //for removing listeners
try{
loadnewmessages();
}catch(error){console.log(error)};
}
}, []);
const pushMessagetoDB = async (filter, imageName) => {
//sending message to the chatroom in Firestore
if(filter == 1){
await firebase.firestore().collection("group_rooms").doc("group_rooms").collection(`${countryId}`).add({
"counter": 1,
"sender": userId,
"timestamp": Date.now(),
"value": TItext,
"displayedTime": (new Date()).toISOString(),
"senderProfilePic": myProfilePic
})
.then(() => {
console.log("Chat written in DB!");
})
.catch((error) => {
console.error("Error writing Chat into DB: ", error);
});
}else{
await firebase.firestore().collection("group_rooms").doc("group_rooms").collection(`${countryId}`).add({
"counter": 2,
"sender": userId,
"timestamp": Date.now(),
"senderProfilePic": myProfilePic,
"value": await firebase.storage().ref(`countrychatimages/${countryId}/${imageName}`).getDownloadURL().then((url) => {
return url
}).catch((error) => { //incase something bad happened
console.log(error);
})
})
.then(() => {
console.log("Image passed to DB!");
})
.catch((error) => {
console.error("Error passing Image to DB: ", error);
});
}
};
What you can see here is my listener loadnewmessages which is beeing called inside my useEffect. This listener downloads the recent 30 messages in the chat and stores them in a state. The chat works perfect and I can even send a message (store a document on the firestore inside a collection which represents the chat). After I leave the screen the return in the useEffect is fired and my listener is getting canceled.
My problem is: I went back and forth around 4 times and I had 6 messages in my collection. After I did that I closed my app and checked my usage in "Usage and billing" in firebase and saw that suddenly I had around 25 reads. I was expecting that my listener will only download the collection with the documents once and will maintain in on the phone even if I leave the screen, not that I redownload it always when I check the screen, that is what I assume is happening after I saw my usage in my firebase console. If I launch my app and I receive 100 or more users, my billings will explode this way.
I know that I detach my listener and relaunch it but I expected firebase to maintain the already loaded data on the phone so I (if no new files will be written) I only get 1 read because the query run without loading any new data.
Can somebody pls explain to me what I did wrong or how I could improve my code to shrink down the reads? How can I change my code so it stays efficient and does not download already loaded data? Its really important for me to maintain the reads on a low level, I have big problems getting this under control and my money is very limited.
That is the intended behavior. When you switch your pages/activities the listener is closed. A listener will fetch all the matching documents specified in query when it's reconnected (just like being connected for first time) as mentioned in the docs:
An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot.
You can try:
Enabling offline persistence which caches a copy of the Cloud Firestore data that your app is actively using, so your app can access the data when the device is offline. If the documents are fetched from the cache then you won't be charged reads. However I am not sure if this will be the best option for your use case.
Storing messages fetched so far in local storage of that platform and then query messages sent after message using the listener. You would have to remove messages from local storage if any message is deleted.
const messagesRef = db..collection("group_rooms").doc("group_rooms").collection(`${countryId}`);
return messagesRef.doc("last_local_msg_id").get().then((doc) => {
// Get all messages sent after last local msg
const newMessagesQuery = messagesRef
.orderBy("timestamp")
.startAt(doc)
.limit(30);
});
Using for example async storage suits good, even increasing the size of the memory of async storage is not a problem so that its possible to store more data and therefore more chats as showed here.

How to get values of object in firestore

Im trying to implement code in Firestore which will get the values of a specific object inside a doc in firestore, unfortunently i couldnt find the way to do it.
This is my query code:
useEffect(() => {
firebase
.firestore()
.collection("users")
.doc(uid)
.collection("confirmed-appointments")
.get()
.then((snapshot) => {
let service = [];
snapshot.forEach((doc) => {
service.push(doc.data());
});
console.log("Services: ", service[0].servicesSelected); //Checking if i can get the serviceSelected Obj
});
}, []);
This is a image of the firestore:
What i want is to get the data of the Red circle object, move it to a local object in the code and then present its data inside the app.
any suggestions?
As far as I can tell from the above images, document 10 contains an array, which means that you will need to index into that array in order to get its elements. You can leverage the following code to fetch the servicesSelected object fields:
import firestore from '#react-native-firebase/firestore';
firestore()
.collection('users')
.doc(uid)
.collection("confirmed-appointments")
.get()
.then(querySnapshot => {
//let service = [];
console.log('Total confirmed appointments: ', querySnapshot.size);
querySnapshot.forEach(documentSnapshot => {
console.log("Services Selected: ", documentSnapshot.data().YOUR_ARRAY[1].servicesSelected);
//service.push(documentSnapshot.data());
//console.log('Appointment ID: ', documentSnapshot.id, documentSnapshot.data());
});
});
Note that I assume that servicesSelected lives at index 1 of YOUR_ARRAY (replace YOUR_ARRAY with its actual name).
You can refer to the officially recommended documentation for more details about React Native for Firebase.

Firebase Cloud Functions: TypeError snapshot.forEach is not a function

I've been struggling to understand why my Firebase cloud function isn't working.
I'm deleting a reserved number in a collection called 'anglerNumbers' when a new user has registered and when that users' document has been created. I use this on the client to make sure a reserved number can't be used twice. I'm following the documentation here: https://firebase.google.com/docs/firestore/query-data/queries?authuser=0 (Using Node.js)
But I keep getting the Error: TypeError: snapshot.forEach is not a function
Here's the function:
exports.newUser = functions.firestore.document('users/{userId}')
.onCreate((snap, context) => {
const newUserNumber = snap.data().anglerNumber;
const anglersRef = admin.firestore().collection('anglerNumbers');
const snapshot = anglersRef.where('anglerNumber', '==', newUserNumber).get();
if (snapshot.empty) {
console.log('No matching documents.');
return;
}
snapshot.forEach(doc => {
console.log(doc.id, '=>', doc.data());
doc.delete();
});
})
It does not console log 'No matching documents'. So there are documents but I can't perform the forEach as indicated by the documentation. What am I missing? Thanks!
in this line in your code:
const snapshot = anglersRef.where('anglerNumber', '==', newUserNumber).get();
You assume that get resolves immediately to a snapshot but in fact get() returns a promise that will resolve into a snap shot. You need to wait for this async function.
Either use await if that is possible in your context or use:
anglersRef.where('anglerNumber', '==', newUserNumber).get().then((snapshot)=>{
//do you processing
});

Firestore onSnapshot does not get initial values when app launches

I'm building a mobile app using React Native and Redux and Firestore. I'm trying to fetch data in my componentDidMount method, but also listen for updates on that data. So I decided to use the onSnapshot method to listen to a users document in my database. So my redux action looks like this:
export const onUserDetailsChange = () => (dispatch) => {
const uid = auth().currentUser.uid;
return firestore()
.collection('users')
.doc(uid)
.onSnapshot(
(documentSnapshot) => {
dispatch({
type: RECEIVED_USER_DETAILS,
payload: documentSnapshot.data(),
});
},
(err) => {
console.log(err);
},
);
};
And my componentDidMount looks like this:
componentDidMount() {
this.userDetailsListener = this.props.onUserDetailsChange();
}
When I launch on the app using npx react-native run-ios, I verified that componentDidMount does get called. And it goes to the action and it even returns an unsubscriber from the onSnapshot. But my store never updates, and even console logs within the onSnapshot don't get called either. This is only on the initial launch. When I refresh the app, then everything works normally.
Is this a firestore issue? Is there something wrong with my onSnapshot? Or is it some sort of npm cache or DerivedData issue?
I've tried searching for this issue but haven't found anyone else who has had trouble getting the initial data fetch from onSnapshot.

How to limit data returned by Firestore's onSnapshot listener?

In the context of react-native chat application project, I'm using
firebase.firestore().collection("conversations").where("members", "array-contains", {email}).onSnapshot( ... ).
This listener is located in my inbox's componentDidMount()
Every time the app launches, the entire result set of the query is returned, even if there is nothing new. How can I limit the results of this listener to only what has changed from the last time it read from firestore? I've looked into redux-persist, but I'm not sure how that would limit the result set of onSnapshot.
The main goal is to minimize a potentially extreme Firebase bill. if it cant be done with onSnapshot, what are some other options that maintain realtime functionality? I'm aware the realtime database charges for storage instead of reads/writes/deletes
Migrating to firebase's realtime database is unnecessary. in worst case scenario (screens with large amounts of data), major firestore-read-savings can be had with the combination of redux-persist and this firestore query:
export const inboxLoadTest = (email, startAfter) => {
return dispatch => {
dispatch({ type: types.INBOX_LOADING });
firebase
.firestore()
.collection("conversations")
.where("members", "array-contains", email)
.orderBy("lastMsgTime")
.startAfter([startAfter])
.onSnapshot(
querySnapshot => {
let conversations = [];
querySnapshot.forEach(queryDocumentSnapshot => {
const membersArr = queryDocumentSnapshot.get("members");
let conversation = {
id: queryDocumentSnapshot.id,
conversant:
membersArr[0] === email ? membersArr[1] : membersArr[0],
lastMsgTime: queryDocumentSnapshot.get("lastMsgTime")
};
conversations.push(conversation);
});
dispatch(loadInboxSuccess(conversations));
},
errorObject => {
dispatch(loadInboxError(errorObject.message));
}
);
};
};
Once data is loaded, redux-persist will save to asyncstorage so on app re-load, the above listener will only fire for messages received after the last message stored in redux/asyncstorage, instead of the entire collection of messages, saving on the result set, and thus the firebase billing

Resources