Trying to join 2 Firestore documents but page freezes - reactjs

I am trying to join 2 Firestore documents with eachother. I could not really figure out what the proper method was, so I figured something out myself. This is the function:
function getRooms() {
setLoading(true);
roomdb.onSnapshot((querySnapshot) => {
const items = [];
querySnapshot.forEach((doc) => {
items.push(doc.data());
});
setRooms(items);
db.collection("Users")
.doc(user?.uid)
.collection("Lijstjes")
.onSnapshot((querySnapshot) => {
const items = [];
querySnapshot.forEach((doc) => {
items.push(doc.data());
});
setUserRoomId(items);
});
for (var i = 0; i < rooms.length; i++) {
for (var y = 0; y < userRoomId.length; i++) {
if (rooms[i]?.id === userRoomId[y]?.id) {
setUserRooms(...userRooms, rooms[i]);
}
}
}
setLoading(false);
});
}
So what I am basically trying to do here is to only show the rooms that the user has in his document. I loop over all the rooms and in that loop I have another loop to see if the room id is the same one that the user has. What am I doing wrong here?
For reference, here is how I structured my data:
Rooms:
Users:
'Lijstjes' means rooms in my language.
I load this function using the useEffect() function.

Unless your app requires live update of the user's rooms, I don't think this is the correct place for an onSnapshot, let alone a nested onSnapshot!
You can get() a user's rooms given their uid with a simple query, then use those returned room IDs to get the room data in another query.
async function getRooms() { // get() queries are async...
// ...so you need to *await* for the following queries
setLoading(true)
return await db.collection("Users")
.doc(user?.uid)
.collection("Lijstjes")
.get() // returns a an array of user's rooms as DocumentReferences
.then(function (querySnapshot) {
const roomPromises = [] // the array where you'll store the .get() requests for rooms
querySnapshot.forEach(function(docRef) {
roomPromises.push(db.collection("rooms").doc(docRef.id).get())
});
return Promise.all(roomPromises) // execution of each room's .get()
})
.then(function (rooms) {
const roomsData = rooms.map(function (room) { return room.data() })
setUserRooms(roomsData)
})
.then(function () {
setLoading(false);
})
}
The basic flow is:
setLoading to true
get() the document references in the user's Lijstjes subcollection
use those document reference IDs to run get() queries for those specific documents in the rooms collection
return the room documents' data (in this case setUserRooms)
once you have the data you need (after all asynchronous functions have completed successfully) then you can setLoading to false
N.B. the Promise.all() pattern is just a way of running the X number of room get() requests in parallel and is a really great practice for speeding up your async functions. That block could also be run sequentially.

Related

How could I write this function so it doesn't setState within the foreach everytime

The function collects role Assignment PrincipalIds on an item in SPO. I then use a foreach to populate state with the Title's of these PrincipalIds. This all works fine but it's inefficient and I'm sure there is a better way to do it than rendering multiple times.
private _permJeChange = async () => {
if(this.state.userNames){
this.setState({
userNames: []
});
}
var theId = this.state.SelPermJEDD;
var theId2 = theId.replace('JE','');
var info = await sp.web.lists.getByTitle('MyList').items.getById(theId2).roleAssignments();
console.log(info, 'info');
var newArr = info.map(a => a.PrincipalId);
console.log(newArr, 'newArr');
// const userIds = [];
// const userNames = [];
// const userNameState = this.state.userNames;
newArr.forEach(async el => {
try {
await sp.web.siteUsers.getById(el).get().then(u => {
this.setState(prevState => ({
userNames: [...prevState.userNames, u.Title]
}));
// userNames.push(u.Title);
// userIds.push(el);
});
} catch (err) {
console.error("This JEForm contains a group");
}
});
}
I've left old code in there to give you an idea of what I've tried. I initially tried using a local variable array const userNames = [] but declaring it locally or even globally would clear the array everytime the array was populated! So that was no good.
PS. The reason there is a try catch is to handle any SPO item that has a permissions group assigned to it. The RoleAssignments() request can't handle groups, only users.
Create an array of Promises and await them all to resolve and then do a single state update.
const requests = info.map(({ PrincipalId }) =>
sp.web.siteUsers.getById(PrincipalId).get().then(u => u.Title)
);
try {
const titles = await Promise.all(requests);
this.setState(prevState => ({
userNames: prevState.userNames.concat(titles),
}));
} catch (err) {
console.error("This JEForm contains a group");
}

Want to set a state object within a for loop: React+Typescript

I have a state object which is basically an array of objects. I am doing a fetch call, and the array of objects returned by it should be set to the state object. But It I am unable to do the same.
Any alternative?
Where am I going wrong?
Help would be appreciated
this.state = {
accounts: [{firstname:"",lastname:"",age:"",id:""}],
};
async componentDidMount(){
let useraccounts = await this.fetchAccounts();
if(useraccounts.length > 0)
{
for(let i=0;i<useraccounts.length;i++)
{
account = {firstname:useraccounts[i].firstname,lastname:useraccounts[i].lastname,age:useraccounts[i].age,id:useraccounts[i].id};
this.setState((prevState) => ({ accounts: [ ...prevState.accounts, account ]}));
}
}
}
fetchAccounts = async() => {
//fetch API call which will return all users accounts
}
You don't need to call setState for each account individually, just do a single call with all of the accounts:
async componentDidMount(){
try {
let useraccounts = await this.fetchAccounts();
let newAccounts = useraccounts.map(({firstname, lastname, age, id}) => ({firstname, lastname, age, id}));
this.setState(({accounts}) => ({accounts: [...accounts, ...newAccounts]}));
} catch (e) {
// Do something with the error
}
}
That gets the accounts, creates a new array with just the relevant properties (what you were doing in your for loop), then calls setState to add the new accounts.
Note that I'm doing destructuring in the parameter lists of the map callback and the setState callback to pick out only the parts of the objects they receive that I want. For instance, this:
let newAccounts = useraccounts.map(({firstname, lastname, age, id}) => ({firstname, lastname, age, id}));
is the same as this:
let newAccounts = useraccounts.map(account => {
return {
firstname: account.firstname,
lastname: account.lastname,
age: account.age,
id: account.id
};
});
It's just a bit more concise.
Of course, if you don't really need to copy the objects, you could just use the accounts you got from fetchAccounts directly:
async componentDidMount(){
try {
let useraccounts = await this.fetchAccounts();
this.setState(({accounts}) => ({accounts: [...accounts, ...useraccounts]}));
} catch (e) {
// Do something with the error
}
}
Some notes on your original code:
You're breaking one of the rules of promises by using an async function where nothing is going to handle the promise it returns: You need to handle any errors that occur, rather than ignoring them. That's why I added a try/catch.
If you're doing for(let i=0;i<useraccounts.length;i++), there's no need for if(useraccounts.length > 0) first. Your loop body won't run if there are no accounts.

Parsing firebase query data in reactjs

I am using firebase cloud firestore for storing data. And I am developing a web app using reactjs. I have obtained documents using the following function:
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
});
console.log(persons);
return persons;
}
I am getting the desired data, but when I am iterating through persons array, it says it has length of 0.
here is the console output when I am displaying complete persons array and its length.
The length should be 14, but it shows 0. Please correct me what is wrong with me?
I want to display the output in the html inside the render() method of react component.
The output of
const peoples = this.getPeoples();
console.log(peoples);
It is:
The complete render method looks like:
render() {
const peoples = this.getPeoples();
console.log(peoples);
return (
<div className="peopleContainer">
<h2>Post-Graduate Students</h2>
{/* <h4>{displayPerson}</h4> */}
</div>
);
}
This is due to the fact the query to the database is asynchronous and you are returning the persons array before this asynchronous task is finished (i.e. before the promise returned by the get() method resolves).
You should return the persons array within the then(), as follows:
getPeoples() {
let persons = [];
return firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
console.log(persons);
return persons;
});
}
And you need to call it as follows, because it will return a promise :
getPeoples().then(result => {
console.log(result);
});
Have a look at what is written to the console if you do:
console.log(getPeoples());
getPeoples().then(result => {
console.log(result);
});
I'm not sure but please try to update your
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
});
console.log(persons);
return persons;
}
to
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(querySnapshot => {
querySnapshot.forEach((doc) => {
persons.push({name = doc.data().Name,
course = doc.data().Course
})
});
console.log(persons);
return persons;
}
Update
Sorry I thought you have problem with filling persons array in your function. Anyway as Renaud mentioned the query in your function is asynchronous so the result is not quick enough to be displayed on render. I use similar function and I found redux the best way to handle this situations.

Cloud Firestore deep get with subcollection

Let's say we have a root collection named 'todos'.
Every document in this collection has:
title: String
subcollection named todo_items
Every document in the subcollection todo_items has
title: String
completed: Boolean
I know that querying in Cloud Firestore is shallow by default, which is great, but is there a way to query the todos and get results that include the subcollection todo_items automatically?
In other words, how do I make the following query include the todo_items subcollection?
db.collection('todos').onSnapshot((snapshot) => {
snapshot.docChanges.forEach((change) => {
// ...
});
});
This type of query isn't supported, although it is something we may consider in the future.
If anyone is still interested in knowing how to do deep query in firestore, here's a version of cloud function getAllTodos that I've come up with, that returns all the 'todos' which has 'todo_items' subcollection.
exports.getAllTodos = function (req, res) {
getTodos().
then((todos) => {
console.log("All Todos " + todos) // All Todos with its todo_items sub collection.
return res.json(todos);
})
.catch((err) => {
console.log('Error getting documents', err);
return res.status(500).json({ message: "Error getting the all Todos" + err });
});
}
function getTodos(){
var todosRef = db.collection('todos');
return todosRef.get()
.then((snapshot) => {
let todos = [];
return Promise.all(
snapshot.docs.map(doc => {
let todo = {};
todo.id = doc.id;
todo.todo = doc.data(); // will have 'todo.title'
var todoItemsPromise = getTodoItemsById(todo.id);
return todoItemsPromise.then((todoItems) => {
todo.todo_items = todoItems;
todos.push(todo);
return todos;
})
})
)
.then(todos => {
return todos.length > 0 ? todos[todos.length - 1] : [];
})
})
}
function getTodoItemsById(id){
var todoItemsRef = db.collection('todos').doc(id).collection('todo_items');
let todo_items = [];
return todoItemsRef.get()
.then(snapshot => {
snapshot.forEach(item => {
let todo_item = {};
todo_item.id = item.id;
todo_item.todo_item = item.data(); // will have 'todo_item.title' and 'todo_item.completed'
todo_items.push(todo_item);
})
return todo_items;
})
}
I have faced the same issue but with IOS, any way if i get your question and if you use auto-ID for to-do collection document its will be easy if your store the document ID as afield with the title field
in my case :
let ref = self.db.collection("collectionName").document()
let data = ["docID": ref.documentID,"title" :"some title"]
So when you retrieve lets say an array of to-do's and when click on any item you can navigate so easy by the path
ref = db.collection("docID/\(todo_items)")
I wish i could give you the exact code but i'm not familiar with Javascript
I used AngularFirestore (afs) and Typescript:
import { map, flatMap } from 'rxjs/operators';
import { combineLatest } from 'rxjs';
interface DocWithId {
id: string;
}
convertSnapshots<T>(snaps) {
return <T[]>snaps.map(snap => {
return {
id: snap.payload.doc.id,
...snap.payload.doc.data()
};
});
}
getDocumentsWithSubcollection<T extends DocWithId>(
collection: string,
subCollection: string
) {
return this.afs
.collection(collection)
.snapshotChanges()
.pipe(
map(this.convertSnapshots),
map((documents: T[]) =>
documents.map(document => {
return this.afs
.collection(`${collection}/${document.id}/${subCollection}`)
.snapshotChanges()
.pipe(
map(this.convertSnapshots),
map(subdocuments =>
Object.assign(document, { [subCollection]: subdocuments })
)
);
})
),
flatMap(combined => combineLatest(combined))
);
}
As pointed out in other answers, you cannot request deep queries.
My recommendation: Duplicate your data as minimally as possible.
I'm running into this same problem with "pet ownership". In my search results, I need to display each pet a user owns, but I also need to be able to search for pets on their own. I ended up duplicated the data. I'm going to have a pets array property on each user AS WELL AS a pets subcollection. I think that's the best we can do with these kinds of scenarios.
According to docs, you needs to make 2 calls to the firestore.. one to fetch doc and second to fetch subcollection.
The best you can do to reduce overall time is to make these two calls parallelly using promise.All or promise.allSettled instead of sequentially.
You could try something like this:
db.collection('coll').doc('doc').collection('subcoll').doc('subdoc')

how to get id of object from firebase database in reactjs

how to get id of object from firebase database in reactjs
I have an array of list got from firebase database in react-redux , I want to get id of every object of array, How can I get?
Get the snapshot, and iterate through it as a Map, with Object.keys(foo).forEach for example.
Here is a dummy piece of code :
`
const rootRef = firebase.database().ref();
const fooRef = rootRef.child("foo");
fooRef.on("value", snap => {
const foo = snap.val();
if (foo !== null) {
Object.keys(foo).forEach(key => {
// The ID is the key
console.log(key);
// The Object is foo[key]
console.log(foo[key]);
});
}
});
`
Be careful with Arrays in Firebase : they are Maps translated into Arrays if the IDs are consecutive numbers started from '0'. If you remove an item in the middle of your array, it will not change the ID accordingly. Better work with Maps, it's more predictable.
You could try something like this:
export const getAllRooms = () => {
return roomCollection.get().then(function (querySnapshot) {
const rooms = [];
querySnapshot.forEach(function (doc) {
const room = doc.data();
room.id = doc.id;
rooms.push(room);
});
return rooms;
});
};
`

Resources