I have two files contact.js and functions.js. I am using firestore realtime functionality.
Here is my functions.js file code:
export const getUserContacts = () => {
const contactDetailsArr = [];
return db.collection("users").doc(userId)
.onSnapshot(docs => {
const contactsObject = docs.data().contacts;
for (let contact in contactsObject) {
db.collection("users").doc(contact).get()
.then(userDetail => {
contactDetailsArr.push({
userId: contact,
lastMessage: contactsObject[contact].lastMsg,
time: contactsObject[contact].lastMsgTime,
userName:userDetail.data().userName,
email: userDetail.data().emailId,
active: userDetail.data().active,
img: userDetail.data().imageUrl,
unreadMsg:contactsObject[contact].unreadMsg
})
})
}
console.log(contactDetailsArr);
return contactDetailsArr;
})
}
in contact.js when I do:
useEffect(() => {
let temp = getUserContacts();
console.log(temp);
}, [])
I want to extract data of contactDetailsArr in contacts.js but I get the value of temp consoled as:
ƒ () {
i.Zl(), r.cs.ws(function () {
return Pr(r.q_, o);
});
}
How do I extract the array data in my case?
The onSnapshot() returns a function that can be used to detach the Firestore listener. When using a listener, it's best to set the data directly into state rather than returning something from that function. Try refactoring the code as shown below:
const [contacts, setContacts] = useState([]);
useEffect(() => {
const getUserContacts = () => {
const contactDetailsArr = [];
const detach = db.collection("users").doc(userId)
.onSnapshot(docs => {
const contactsObject = docs.data().contacts;
const contactsSnap = await Promise.all(contactsObject.map((c) => db.collection("users").doc(c).get()))
const contactDetails = contactsSnap.map((d) => ({
id: d.id,
...d.data()
// other fields like unreadMsg, time
}))
// Update in state
setContacts(contactDetails);
})
}
getUserContacts();
}, [])
Then use contacts array to map data in to UI directly.
Assumptions
This answer assumes a user's data looks like this in your Firestore:
// Document at /users/someUserId
{
"active": true,
"contacts": {
"someOtherUserId": {
"lastMsg": "This is a message",
"lastMsgTime": /* Timestamp */,
"unreadMsg": true // unclear if this is a boolean or a count of messages
},
"anotherUserId": {
"lastMsg": "Hi some user! How are you?",
"lastMsgTime": /* Timestamp */,
"unreadMsg": false
}
},
"emailId": "someuser#example.com",
"imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
"userName": "Some User"
}
Note: When asking questions in the future, please add examples of your data structure similar to the above
Attaching Listeners with Current Structure
The structure as shown above has a number of flaws. The "contacts" object in the user's data should be moved to a sub-collection of the user's main document. The reasons for this include:
Any user can read another user's (latest) messages (which can't be blocked with security rules)
Any user can read another user's contacts list (which can't be blocked with security rules)
As an individual user messages more users, their user data will grow rapidly in size
Each time you want to read a user's data, you have to download their entire message map even if not using it
As you fill out a user's contacts array, you are fetching their entire user data document even though you only need their active, email, imageUrl, and userName properties
Higher chance of encountering document write conflicts when two users are editing the contact list of the same user (such as when sending a message)
Hard to (efficiently) detect changes to a user's contact list (e.g. new addition, deletion)
Hard to (efficiently) listen to changes to another user's active status, email, profile image and display name as the listeners would be fired for every message update they receive
To fetch a user's contacts once in your functions.js library, you would use:
// Utility function: Used to hydrate an entry in a user's "contacts" map
const getContactFromContactMapEntry = (db, [contactId, msgInfo]) => {
return db.collection("users")
.doc(contactId)
.get()
.then((contactDocSnapshot) => {
const { lastMsg, lastMsgTime, unreadMsg, userName } = msgInfo;
const baseContactData = {
lastMessage: lastMsg,
time: lastMsgTime,
unreadMsg,
userId: contactId
}
if (!contactDocSnapshot.exists) {
// TODO: Decide how to handle unknown/deleted users
return {
...baseContactData,
active: false, // deleted users are inactive, nor do they
email: null, // have an email, image or display name
img: null,
userName: "Deleted user"
};
}
const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
return {
...baseContactData,
active,
email: emailId,
img: imageUrl,
userName
};
});
};
export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
return db.collection("users")
.doc(userId)
.get()
.then((userDataSnapshot) => {
const contactsMetadataMap = userDataSnapshot.get("contacts");
return Promise.all( // <-- waits for each Promise to complete
Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
.map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
});
}
Example Usage:
getUserContacts(db, userId)
.then((contacts) => console.log("Contacts data:", contacts))
.catch((err) => console.error("Failed to get contacts:", err))
// OR
try {
const contacts = await getUserContacts(db, userId);
console.log("Contacts data:", contacts);
} catch (err) {
console.error("Failed to get contacts:", err)
}
To fetch a user's contacts, and keep the list updated, using a function in your functions.js library, you would use:
// reuse getContactFromContactMapEntry as above
export const useUserContacts = (db, userId) => {
if (!db) throw new TypeError("Parameter 'db' is required");
const [userContactsData, setUserContactsData] = useState({ loading: true, contacts: [], error: null });
useEffect(() => {
// no user signed in?
if (!userId) {
setUserContactsData({ loading: false, contacts: [], error: "No user signed in" });
return;
}
// update loading status (as needed)
if (!userContactsData.loading) {
setUserContactsData({ loading: true, contacts: [], error: null });
}
let detached = false;
const detachListener = db.collection("users")
.doc(userId)
.onSnapshot({
next: (userDataSnapshot) => {
const contactsMetadataMap = userDataSnapshot.get("contacts");
const hydrateContactsPromise = Promise.all( // <-- waits for each Promise to complete
Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
.map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
hydrateContactsPromise
.then((contacts) => {
if (detached) return; // detached already, do nothing.
setUserContactsData({ loading: false, contacts, error: null });
})
.catch((err) => {
if (detached) return; // detached already, do nothing.
setUserContactsData({ loading: false, contacts: [], error: err });
});
},
error: (err) => {
setUserContactsData({ loading: false, contacts: [], error: err });
}
});
return () => {
detached = true;
detachListener();
}
}, [db, userId])
}
Note: The above code will not (due to complexity):
react to changes in another user's active status, email or profile image
properly handle when the setUserContactsData method is called out of order due to network issues
handle when db instance is changed on every render
Example Usage:
const { loading, contacts, error } = useUserContacts(db, userId);
Attaching Listeners with Sub-collection Structure
To restructure your data for efficiency, your structure would be updated to the following:
// Document at /users/someUserId
{
"active": true,
"emailId": "someuser#example.com",
"imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
"userName": "Some User"
}
// Document at /users/someUserId/contacts/someOtherUserId
{
"lastMsg": "This is a message",
"lastMsgTime": /* Timestamp */,
"unreadMsg": true // unclear if this is a boolean or a count of messages
}
// Document at /users/someUserId/contacts/anotherUserId
{
"lastMsg": "Hi some user! How are you?",
"lastMsgTime": /* Timestamp */,
"unreadMsg": false
}
Using the above structure provides the following benefits:
Significantly better network performance when hydrating the contacts list
Security rules can be used to ensure users can't read each others contacts lists
Security rules can be used to ensure a message stays private between the two users
Listening to another user's profile updates can be done without reading or being notified of any changes to their other private messages
You can partially fetch a user's message inbox rather than the whole list
The contacts list is easy to update as two users updating the same contact entry is unlikely
Easy to detect when a user's contact entry has been added, deleted or modified (such as receiving a new message or marking a message read)
To fetch a user's contacts once in your functions.js library, you would use:
// Utility function: Merges the data from an entry in a user's "contacts" collection with that user's data
const mergeContactEntrySnapshotWithUserSnapshot = (contactEntryDocSnapshot, contactDocSnapshot) => {
const { lastMsg, lastMsgTime, unreadMsg } = contactEntryDocSnapshot.data();
const baseContactData = {
lastMessage: lastMsg,
time: lastMsgTime,
unreadMsg,
userId: contactEntryDocSnapshot.id
}
if (!contactDocSnapshot.exists) {
// TODO: Handle unknown/deleted users
return {
...baseContactData,
active: false, // deleted users are inactive, nor do they
email: null, // have an email, image or display name
img: null,
userName: "Deleted user"
};
}
const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
return {
...baseContactData,
active,
email: emailId,
img: imageUrl,
userName
};
}
// Utility function: Used to hydrate an entry in a user's "contacts" collection
const getContactFromContactsEntrySnapshot = (db, contactEntryDocSnapshot) => {
return db.collection("users")
.doc(contactEntry.userId)
.get()
.then((contactDocSnapshot) => mergeContactEntrySnapshotWithUserSnapshot(contactEntryDocSnapshot, contactDocSnapshot));
};
export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
return db.collection("users")
.doc(userId)
.collection("contacts")
.get()
.then((userContactsQuerySnapshot) => {
return Promise.all( // <-- waits for each Promise to complete
userContactsQuerySnapshot.docs // <-- used to get an array of entry snapshots that we can iterate over
.map(getContactFromContactsEntrySnapshot.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
});
}
Example Usage:
getUserContacts(db, userId)
.then((contacts) => console.log("Contacts data:", contacts))
.catch((err) => console.error("Failed to get contacts:", err))
// OR
try {
const contacts = await getUserContacts(db, userId);
console.log("Contacts data:", contacts);
} catch (err) {
console.error("Failed to get contacts:", err)
}
To fetch a user's contacts in a way where it's kept up to date, we first need to introduce a couple of utility useEffect wrappers (there are libraries for more robust implementations):
export const useFirestoreDocument = ({ db, path }) => {
if (!db) throw new TypeError("Property 'db' is required");
const [documentInfo, setDocumentInfo] = useState({ loading: true, snapshot: null, error: null });
useEffect(() => {
if (!path) {
setDocumentInfo({ loading: false, snapshot: null, error: "Invalid path" });
return;
}
// update loading status (as needed)
if (!documentInfo.loading) {
setDocumentInfo({ loading: true, snapshot: null, error: null });
}
return db.doc(path)
.onSnapshot({
next: (docSnapshot) => {
setDocumentInfo({ loading: false, snapshot, error: null });
},
error: (err) => {
setDocumentInfo({ loading: false, snapshot: null, error: err });
}
});
}, [db, path]);
return documentInfo;
}
export const useFirestoreCollection = ({ db, path }) => {
if (!db) throw new TypeError("Property 'db' is required");
const [collectionInfo, setCollectionInfo] = useState({ loading: true, docs: null, error: null });
useEffect(() => {
if (!path) {
setCollectionInfo({ loading: false, docs: null, error: "Invalid path" });
return;
}
// update loading status (as needed)
if (!collectionInfo.loading) {
setCollectionInfo({ loading: true, docs: null, error: null });
}
return db.collection(path)
.onSnapshot({
next: (querySnapshot) => {
setCollectionInfo({ loading: false, docs: querySnapshot.docs, error: null });
},
error: (err) => {
setCollectionInfo({ loading: false, docs: null, error: err });
}
});
}, [db, path]);
return collectionInfo;
}
To use that method to hydrate a contact, you would call it from a ContactEntry component:
// mergeContactEntrySnapshotWithUserSnapshot is the same as above
const ContactEntry = ({ db, userId, key: contactId }) => {
if (!db) throw new TypeError("Property 'db' is required");
if (!userId) throw new TypeError("Property 'userId' is required");
if (!contactId) throw new TypeError("Property 'key' (the contact's user ID) is required");
const contactEntryInfo = useFirestoreDocument(db, `/users/${userId}/contacts/${contactId}`);
const contactUserInfo = useFirestoreDocument(db, `/users/${contactId}`);
if ((contactEntryInfo.loading && !contactEntryInfo.error) && (contactUserInfo.loading && !contactUserInfo.error)) {
return (<div>Loading...</div>);
}
const error = contactEntryInfo.error || contactUserInfo.error;
if (error) {
return (<div>Contact unavailable: {error.message}</div>);
}
const contact = mergeContactEntrySnapshotWithUserSnapshot(contactEntryInfo.snapshot, contactUserInfo.snapshot);
return (<!-- contact content here -->);
}
Those ContactEntry components would be populated from a Contacts component:
const Contacts = ({db}) => {
if (!db) throw new TypeError("Property 'db' is required");
const { user } = useFirebaseAuth();
const contactsCollectionInfo = useFirestoreCollection(db, user ? `/users/${user.uid}/contacts` : null);
if (!user) {
return (<div>Not signed in!</div>);
}
if (contactsCollectionInfo.loading) {
return (<div>Loading contacts...</div>);
}
if (contactsCollectionInfo.error) {
return (<div>Contacts list unavailable: {contactsCollectionInfo.error.message}</div>);
}
const contactEntrySnapshots = contactsCollectionInfo.docs;
return (
<>{
contactEntrySnapshots.map(snapshot => {
return (<ContactEntry {...{ db, key: snapshot.id, userId: user.uid }} />);
})
}</>
);
}
Example Usage:
const db = firebase.firestore();
return (<Contacts db={db} />);
Your code seems to be not written with async/await or promise like style
e.g. contactDetailsArr will be returned as empty array
also onSnapshot creates long term subscription to Firestore collection and could be replaced with simple get()
See example on firestore https://firebase.google.com/docs/firestore/query-data/get-data#web-version-9_1
I am creating MongoDB records based on user inputs, and then for each new object created, I am pushing the object ID into an array. My problem is my console.log statement in the last line returns empty. So How I could wait on the forEach execution to be over so I have the updated assetsArray, any help is appreciated!
Here is my code:
let assetsArray = [];
if (assets) {
JSON.parse(assets).forEach(asset => {
Image.create({
organization: organizationID,
imageName: asset.imageName,
imageDescription: asset.imageDescription ?? null,
imagePath: asset.imagePath
}).then(record => {
console.log(record)
assetsArray.push(record._id)
})
})
}
console.log(assetsArray)
You can use Promise.all for your case
const tasks = JSON.parse(assets).map(asset => {
return Image.create({
organization: organizationID,
imageName: asset.imageName,
imageDescription: asset.imageDescription ?? null,
imagePath: asset.imagePath,
})
});
Promise.all(tasks).then((records) => {
return records.map(record => {
console.log(record)
return record._id
})
}).then(assetsArray => {
console.log(assetsArray)
})
I'm trying to filter a players array by comparing the names property to a roster array and rendering the matches from the players array.
When a user selects a year the getCollegeRoster() function returns players who have a year property matching the selection. From there I'm trying to filter and display the .name matches from the players array, but it seems i'm unable to update playerStateValue. I'm using recoil to manage state. Any insight would be much appreciated.
const getCollegeRoster = async () => {
setLoading(true);
try {
const playersQuery = query(
collection(firestore, `colleges/${communityData.id}/rosters`),
where("year", "==", selection),
);
const playerDocs = await getDocs(playersQuery);
const collegeRoster = playerDocs.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setRoster(collegeRoster as unknown as Roster[]);
console.log('collegePlayers', roster);
} catch (error: any) {
console.log("getCollegePlayers error", error.message);
}
setLoading(false);
};
const onSelection = (newSelection: any) => {
setSelection(newSelection);
console.log('newselect', newSelection)
getCollegeRoster();
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name)
})
);
setPlayerStateValue((prev) => ({
...prev,
players: filteredArray as Player[],
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray as Player[],
},
playerUpdateRequired: false,
}));
} ```
also tried adding setplayerstatevalue into the getcollegeroster function:
onst getCollegeRoster = async () => {
console.log("WE ARE GETTING Players!!!");
console.log('communitydata', communityData);
setLoading(true);
try {
const playersQuery = query(
collection(firestore, `colleges/${communityData.id}/rosters`),
where("year", "==", selection),
);
const playerDocs = await getDocs(playersQuery);
const collegeRoster = playerDocs.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setRoster(collegeRoster as unknown as Roster[]);
console.log('collegePlayers', roster);
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name)
})
);
setPlayerStateValue((prev) => ({
...prev,
players: filteredArray as Player[],
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray as Player[],
},
playerUpdateRequired: false,
}));
} catch (error: any) {
console.log("getCollegePlayers error", error.message);
}
setLoading(false);
};
{playerStateValue.players.map((player: Player, index) => (
<PlayerItem
key={player.id}
player={player}
// postIdx={index}
onPlayerVote={onPlayerVote}
userVoteValue={
playerStateValue.playerVotes.find((item) => item.playerId === player.id)
?.voteValue
}
onSelectPlayer={onSelectPlayer}
/>
I believe I found the problem. getCollegeRoster() is an async function which means it is being executed asynchronous.
That function is responsible for updating roster and you are using roster inside your filtering while it is being asynchronously updated.
There is no guarantee that roster is updated by the time you filter your array.
You have two ways of solving this. Either you execute setPlayerStateValue() inside of getCollegeRoster() or you wait for getCollegeRoster() to be done by preappending it with await and your onSelection function with async:
const onSelection = async (newSelection: any) => {
setSelection(newSelection);
console.log("newselect", newSelection);
await getCollegeRoster();
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name),
}));
...
Edit:
Another scenario I can think of is that playerStateValue might be the culprit. In React setState works asynchronous as well, if you access playerStateValue via curr/prev-callback, it is guaranteed that you are accessing the values of the current state object while updating the state. Try this use of setPlayerStateValue:
setPlayerStateValue((prev) => {
const filteredArray = prev.players.filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name),
})) as Player[];
return {
...prev,
players: filteredArray,
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray,
},
playerUpdateRequired: false,
};
});
As I am new in Vuejs, It will be very easy for others, but for me I cannot get it right, I search a lot but cannot get the expected answer
Below is my code
watch: {
$route () {
this.newsData = []
this.loadNewsByCategory()
this.category = {}
this.getCategoryData()
}
},
created () {
this.getCategoryData()
this.loadNewsByCategory()
},
methods () {
async getCategoryData() {
// console.log('called category data')
try {
await firebase.firestore().collection('categories').doc(this.$route.params.id).get().then((doc) => {
if (doc.exists) {
this.category = doc.data()
}
})
} catch (error) {
console.log(error) // this line show the error I've post below
this.$q.notify({ type: 'negative', message: error.toString() })
}
},
async loadNewsByCategory() {
// console.log('load news')
try {
var db = firebase.firestore()
var first = db.collection('posts')
.where('publishedAt', '<=', new Date())
.where('categories', 'array-contains', this.$route.params.id)
.orderBy('publishedAt', 'desc')
.limit(12)
return await first.get().then((documentSnapshots) => {
documentSnapshots.forEach((doc) => {
const news = {
id: doc.id,
slug: doc.data().slug,
title: doc.data().title,
image: doc.data().image.originalUrl,
publishedAt: dateFormat(doc.data().publishedAt.toDate(), 'dS mmm, yyyy')
}
this.newsData.push(news)
})
this.lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1]
})
} catch (error) {
this.$q.notify({ type: 'negative', message: error.toString() })
}
}
}
In my code, when initialize I call two functions to query data from firebase and it's working as expected. but when the route change for example: from getdata/1 to getdata/2 one function i.e., loadNewsByCategory() is working but others throw error like below
TypeError: u.indexOf is not a function
at VueComponent.getCategoryData (getdata.vue?3b03:102)
at VueComponent.created (getdata.vue?3b03:92)
Thank you in advance
I am using promises. This is in continuation to my question here
The issue I am having is that in response, i.e. an array of objects is having null values. I will try to explain this
First I get the userId
Get user whishlist products from the userId
Then using userId I get stores/shop list
Then I iterate over store list and call another API to check if this store is user favourite store.
Then I get the products of each store and append in an object and return.
function getStoresList(context) {
const userID = common.getUserID(context)
let userWishListProd = []
return userID
.then(uid => Wishlist.getUserWishlistProducts(uid).then((products) => {
userWishListProd = products.data.map(product => +product.id)
return uid
}))
.then(uid => api.getOfficialStoresList(uid).then((response) => {
if (!response.data) {
const raw = JSON.stringify(response)
return []
}
const shops = response.data
return Promise.all(
shops.map((shop) => {
const id = shop.shop_id
const shopobj = {
id,
name: shop.shop_name,
}
return favAPI.checkFavourite(uid, id)
.then((favData) => {
shopobj.is_fave_shop = favData
// Fetch the products of shop
return getProductList(id, uid)
.then((responsedata) => {
shopobj.products = responsedata.data.products.map(product => ({
id: product.id,
name: product.name,
is_wishlist: userWishListProd.indexOf(product.id) > -1,
}))
return shopobj
})
.catch(() => {})
})
.catch(err => console.error(err))
}))
.then(responses => responses)
.catch(err => console.log(err))
})
.catch(() => {}))
.catch()
}
The response I get is
[{
"id": 1001,
"name": "Gaurdian Store",
"is_fave_shop": "0",
"products": [{
"id": 14285912,
"name": "Sofra Cream",
"is_wishlist": false
}]
},
null,
null,
{
"id": 1002,
"name": "decolite",
"is_fave_shop": "1",
"products": [{
"id": 14285912,
"name": "Denca SPF",
"is_wishlist": false
}]
}
]
The actual store are coming as 4 but instead of it null gets appended. What is wrong I am doing with Promises here.
This appears to have to do with your .catch(() => {}) and .catch(err => console.error(err)) invocations. If one promise in your loop has an error, it will be transformed to an undefined value (optionally being reported before), so that Promise.all will fulfill with an array that might contain undefined values. If you JSON.stringify that, you'll get null at those indices.
Drop the .catch(() => {}) statements that do nothing (or replace them with logging ones), and check your error logs.
Have you debug your code?
I would debug on Chrome and add some break points on the code to see what the actual response is.