How to use a Firestore listener 'ref' in other composables? - firebase

I am using Firestore realtime listeners in my Vue 3 / TypeScript app.
I created a composable that returns a ref to a user doc.
export const getUserListener = (userId: string) => {
const user = ref<User>();
const userDocRef = doc(db, 'users', userId);
const unsubscribe = onSnapshot(
userDocRef,
(snapshot) =>
(user.value = {
...snapshot.data(),
id: snapshot.id,
})
);
onUnmounted(unsubscribe);
return user;
};
It works great when populating the front end with user doc values.
But now I want to use those values in another composable, like so:
export const doSomethingWithUserListener = () => {
const user = useGetUserListener('user-id-abc-123');
if (user.value) {
console.log('User found');
} else {
console.log('User not found');
}
}
And this always returns User not found.
I already know why this is happening...
When getUserListener() first runs the user ref is undefined. So the ref returns as undefined immediately.
The onSnapshot part is asynchronous, so it executes some milliseconds later, and only then populates the ref with user data.
This is not an issue when populating fields on the front end, because users do not perceive the delay. But it causes an issue when using the listener in other composables, because the ref value is always undefined.
Since I known why this is happening I'm not looking for an explanation, but rather help with how to overcome this problem.
Summary: I want to use the listener ref value in other composables. But I can't because it always returns undefined.
Any ideas?

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 load 2 different Firestore docs in one 'onUpdate' Cloud Function?

I am trying to make an "onUpdate" function that loads the document that has been updated. Then I want to load another document using the data received by the wildcards. So to summarize I want to access the document that was updated and one more that is in the same collection.
I want : /userProfiles/{doc1}/employees/{doc2} AND /userProfiles/{doc1}.
I can get them both but when I try to use the data from one, it doesn't read the previous data and gives me a ReferenceError.
The end goal is to use both these docs to send an email with nodemailer. Thanks for any help.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const nodemailer = require('nodemailer');
admin.initializeApp();
exports.testLog = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
var info = [];
const doc1 = context.params.doc1;
const doc2 = context.params.doc2;
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
var email = doc.data().email;
var phone = doc.data().phone;
info.push(doc.data());
console.log(email, phone); // sees and gets info
return email, phone;
}),
db
.collection("userProfiles")
.doc(`${doc1}`)
.collection(`employees`)
.doc(`${doc2}`)
.get()
.then(doc => {
info.push(doc.data());
var Status = doc.data().Status;
console.log(phone, `${Status}`); //phone is undefined
if (`${Status}` === "Alarm") {
// replace with variables from the users settings page
console.log(`${info.phone}`); // phone is undefined
let transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: {
user: "xxxxxx#gmail.com",
pass: "xxxxxxxxxx"
}
});
// send mail with defined transport object
let mailOptions = {
from: '"Fred Foo 👻" <foo#example.com>',
to: `${info.phone}`, // tried phone as well
subject: "Hello ✔",
text: "216+?",
};
transporter.sendMail(mailOptions, error => {
if (error) {
return console.log(error);
} else {
return console.log("message sent");
}
});
}
console.log(Status);
// return
return console.log("im after the if statement. No alarm triggered");
})
.then(message => console.log(message.sid, "success"))
.catch(err => console.log(err))
);
});
So I want to get the phone number and the Status in these 2 images
The error that is returned:
ReferenceError: phone is not defined
There are two things that aren't quite working the way you expect leading to your problem:
The handling of promises isn't really passing data the way you expect -- in particular, the variables phone and email exist only in one promise handler, they aren't global in scope, so phone and email aren't being passed down the promise chain.
You don't actually need to ever read the second document, as the content is passed to you in the function itself. This actually greatly simplifies the overall thing you are doing, and makes dealing with the first point nearly trivial, since you can skip the second database call.
Look at this code where I have omitted the messaging code for clarity and just left in place most of the log messages:
exports.firestoreOnUpdateTest = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
// var info = []; I have removed this list, it is not necessary
const doc1 = context.params.doc1;
// no need to get the doc2 parameter, as we are handed the doc itself by the function call.
const doc2content = change.after.data();
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
const doc1content = doc.data();
const email = doc1content.email;
const phone = doc1content.phone;
console.log(email, phone); // sees and gets info
console.log(`No need to fetch doc2, as I already have it: ${JSON.stringify(doc2content)}`);
const Status = doc2content.Status;
console.log(`email for user is still: ${email}`); // email is now defined
console.log(phone, `${Status}`); // phone is now defined
if (`${Status}` === "Alarm") {
console.log(`${phone}`); // phone is now defined
return console.log('message would be sent here - code omitted')
}
console.log(Status);
return console.log("im after the if statement. No alarm triggered");
})
.catch(err => console.error(err))
);
});
In the new version, we just store the content from the document that triggered us, including the Status parameter. We then fetch the document with the content we need -- at the higher level in the tree. Once that document is returned, we just process it and combine with the data from doc2. All the fields are now defined (assuming, of course, the database objects are well-formed).
Your messaging code would be re-inserted right were the obvious log message is.
Finally, the info list I don't think is necessary now, so I've removed it. Instead, I recommend you build what you need as you construct the message itself from the data already on hand. That said, your original code wasn't accessing it correctly (that is, as a list) anyway and may have been confusing you further.
Finally, I haven't addressed the use of the Nodemailer module as the question focused primarily on the undefined fields, but I suspect your original code may not be entirely correct either -- as it doesn't either return a promise back from sendMail() or perform an await on that call (and make the entire function async), so you will need to look at that more closely.

Cloud Functions for Firebase Time Out w/ wrong response

Newbie question: Cloud Function times out every single time I run it.
In addition, it only returns ONE value, which is the first userId, in the Functions Log and none of its children. Im assuming this is because it's calling the .once however, it's in a forEach loop, so I'm not sure what it wants.
Firebase database
-items
---- userId0123456789
---- randomKey987654321
-- itemName
-- itemDate
-- itemType
---- userId987654321
---- randomKey012345678
-- itemName
-- itemDate
-- itemType
And here is the function code...
const key = req.query.key;
**let userID = 'xxxxx';
let ikey = 'xxx';**
var dbRef = admin.database().ref('/items/{userID}/{ikey}');
dbRef.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(child.key+": "+child.val());
});
});
UPDATE: here is the entire function and now it's just timing out with no response.
'use strict';
// Firebase Functions
const functions = require('firebase-functions');
// Firebase Admin
const admin = require('firebase-admin');
// Default admin firebase configuration
admin.initializeApp(functions.config().firebase);
const rp = require('request-promise');
const promisePool = require('es6-promise-pool');
const PromisePool = promisePool.PromisePool;
const secureCompare = require('secure-compare');
const MAX_CONCURRENT = 3;
//Initial function call:
exports.CheckItemTypeinFB = functions.https.onRequest((req, res) => {
const key = req.query.key;
// Exit if the keys don't match
if (!secureCompare(key, functions.config().cron.key)) {
console.log('The key provided in the request does not match the key set in the environment. Check that', key,
'matches the cron.key attribute in `firebase env:get`');
res.status(403).send('Security key does not match. Make sure your "key" URL query parameter matches the ' +
'cron.key environment variable.');
return;
}
// Try the database here...
let userID = 'xxx';
let ikey = 'xxxxx
//create database ref
let ref = admin.database().ref(`/items/${userID}/${ikey}`);
//do a bunch of stuff
ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
res.send(200, {/* response data */});
});
//send back response
// res.redirect(200);
}) // END THE MAJJOR CONTAINER THINGS
// Returns an access token using the Google Cloud metadata server. */
function getAccessToken(accessToken) {
// If we have an accessToken in cache to re-use we pass it directly.
if (accessToken) {
return Promise.resolve(accessToken);
}
const options = {
uri: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token',
headers: {'Metadata-Flavor': 'Google'},
json: true
};
return rp(options).then(resp => resp.access_token);
}
Help is much appreciated.
Update:. Timeout is fixed and it returns the userId's that are in the database under "/items". HOWEVER, if I use ${userId}/${key} I get nothing. I'm still not able to tell how to get the children under random userId's in the database and none of the other posts I read explain it. Firebase's docs state to use {userId} to get all under that wildcard but its not working. What am I missing?
You're not returning the result of the once function or returning at all, so the function doesn't know when to finish hence the timeout.
let userID = 'xxxxxxxxx';
let key = 'xxxxxxxx';
let ref = admin.database().ref(`/items/${userID}/${key}`);
return ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
});
Also please be aware that the reference you are observing will give you the children of a particular user item (itemName, itemDate, itemType). If you want the items belonging to a particular user, adjust your reference path to be /items/${userID}.
When inside a HTTP trigger, you can send a response after observing the value.
exports.CheckItemTypeinFB = functions.https.onRequest((req, res) => {
...
ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
res.send(200, {/* response data */});
});
});

Redux Thunk Firebase - fetch arrays

I'm building a react native app with redux, react-redux, redux-thunk, and using firebase as a backend.
My database is denormalized, so I've to fetch keys in a firebase reference and for each key I need to fetch data from another reference.
Using firebase .once('value') I got something like this:
const fetchPosts = ( uid ) => {
return ( dispatch ) => {
dispatch(postsIsFetching())
const userPostsRef = firebase.database().ref('users/' + uid + '/myposts')
var keys = []
//
userPostsRef.once('value').then(snap => {
snap.forEach(post => keys.push(post.key))
}).then(() => {
keys.forEach(key => {
const postRef = firebase.database().ref('posts/' + key )
postRef.once('value').then(snap => {
var newPost = {
title: snap.val().title,
user: snap.val().user
}
dispatch(setPost(newPost))
})
})
}
}
But that's not realtime updating, cause the .once() method read data once only.
The .on() method doesn't return a Promise, so how could I fix that?
What's the best pattern to fetch data with redux-thunk and a denormalized firebase database?
Thanks guys
Put the code you'd normally put in a .then() inside the .on() callback. This way, every time the data refreshes, your setPost action will be dispatched.
For example:
...
postRef.on('value', snap => {
var newPost = {
title: snap.val().title,
user: snap.val().user
}
dispatch(setPost(newPost))
});
You probably want to hold on to that database reference so that you can stop listening later.

Sorting a map of keys and values please to order a list in ES6 with Lodash (Keeping Firebase Key: UID)?

I'm currently trying to sort a list of employees in order of names but I'm having difficulty in such.
I'm using React-Native, with ES6, Lodash and firebase.
Firebase structure appears as:
The state.employees structure appears as:
{"-KgMYnBrqXCIPqjs0n7x":{"name":"James","phone":"123766","shift":"Monday"}, "-KgRiK6qiJsoZ_HBXt7K":{"name":"Nick","phone":"123767","shift":"Tuesday"},"-KgRiM77VTOejvYPWPIp":{"name":"Henry","phone":"123","shift":"Thursday"},"-KgRiOaN14OeSjaYWb1O":{"name":"Charlie","phone":"5643","shift":"Saturday"}}
and continues with more employees each having a UID and then properties such as name, phone and shift.
Although I am able to order the list with _.orderBy provided by Lodash, it appears that when doing so the UID as the key provided by firebase is removed when applied say before _.map.
This is what I currently have which orders but the UID is removed, the _.map works fine keeping the UID with the properties but is unordered.
const mapStateToProps = state => {
const sortedEmployees = _.orderBy(
state.employees,
[employee => employee.name.toLowerCase()]
);
const employees = _.map(sortedEmployees, (val, uid) => {
return { ...val, uid };
});
return { employees };
};
Thanks very much
_.orderBy(collection, [iteratees=[_.identity]], [orders]) https://lodash.com/docs/4.17.4#orderBy
It seems when lodash maps over collection of type Object, it omits its key.
One way to fix would be to convert the collection of object from firebase response to an array . Ex:
const arrWithKeys = Object.keys(state.employees).map((key) => {
return {
key,
...state.employees[key]
}
});
and then use
const sortedEmployees = _.orderBy(
arrWithKeys,
[employee => employee.name.toLowerCase()]
);
You can also use firebase sorting in your firebase query
https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data
Thanks for your response #agent_hunt. I tried implementing it as suggested having the same structure as that of _.map but it then complained about TypeError: "Cannot convert undefined or null to object" where it is not null nor undefined and actually matches the original _.map but this time ordered.
To stop it complaining about, I added as a final thought the _.map again but this time taking the sortedEmployees and returning { ...val, uid} and it works.
Couldn't get the firebase query approach to work, as I had to order by a child of a child (key) not knowing the key for each employee.
Here's what I implemented following your above arrWithKeys and sortedEmployees adding _.map as an after thought works
const mapStateToProps = state => {
const arrWithKeys = Object.keys(state.employees).map((uid) => {
return {
...state.employees[uid],
uid,
};
});
const sortedEmployees = _.orderBy(
arrWithKeys,
[employee => employee.name.toLowerCase()],
);
const employees = _.map(sortedEmployees);
return { employees };
};
https://lodash.com/docs/4.17.4#map
https://lodash.com/docs/4.17.4#orderBy

Resources