Firestore .startAt is not working properly - javascript

infiniteHandler($state) {
var next = db
.collection("posts")
.orderBy("timestamp", "desc")
.startAfter(this.lastVisible)
.limit(3)
next.get().then(documentSnapshots => {
//Get the last visible document
// this.lastVisible =
// documentSnapshots.docs[documentSnapshots.docs.length - 1]
if (documentSnapshots.docs.length == 0) $state.complete()
else {
this.$store.commit(
"modules/posts/updateLastVisible",
documentSnapshots.docs[documentSnapshots.docs.length - 1].data()
.timestamp
)
}
documentSnapshots.forEach(doc => {
var post = doc.data()
post.docID = doc.id
this.$store.commit("modules/posts/pushPost", post)
})
$state.loaded()
})
}
This is my infinite loading handler which fetches new DB Entries once the end of the list is reached. Working fine so far.
This is my first fetch when the page gets loaded
async fetch({ store }){
if (store.state.modules.posts.posts.length < 5) {
let posts = []
await db
.collection("posts")
.orderBy("timestamp", "desc")
.limit(3)
.get()
.then(querySnapshot => {
store.commit(
"modules/posts/updateLastVisible",
querySnapshot.docs[2].data().timestamp
)
querySnapshot.forEach(doc => {
var x = doc.data()
x.docID = doc.id
posts.push(x)
})
})
store.commit("modules/posts/fetchedPosts", posts)
}
}
Basicly the problem is that I get the first 3 entries which I fetch on the page load again when I am fetching in my infinite Loading handler, which leads to the entries being displayed twice, this should not happen because this.lastVisible has the timestamp of the 3rd Element that I fetch on load, so those should be ignored.
After those elements everything is working fine with the .startAfter but the first 3 getting loaded again makes no sense.
I checked the store with the devtools and everything is working fine, this.lastVisible has the correct value when the infiniteLoading Handler is called the first time.
Bounty Edit:
Okay so I still have the problem I tried to play around with it a bit more to find the issue but its still occuring... I will set a bounty now and I hope anyone is able to help.

You do not actually need the first time fetch. The infiniteHandler will be called on its own when it gets mounted. In case if it does not call then you can try using the function
this.$refs.infiniteLoading.attemptLoad(); // 'infiniteLoading' is the component's ref property
That will actually invoke the infiniteHandler function for you.
EDIT: To check if one of the function is currently running. On the handler part
infiniteHandler($state) {
//Check if its currently loading
this.$nextTick(()=>{
if (this.isDocSnapShotLoading){
return;
}
});
//set as currently loading
this.isDocSnapShotLoading = true;
var next = db
.collection("posts")
.orderBy("timestamp", "desc")
.startAfter(this.lastVisible)
.limit(3)
next.get().then(documentSnapshots => {
//Get the last visible document
// this.lastVisible =
// documentSnapshots.docs[documentSnapshots.docs.length - 1]
if (documentSnapshots.docs.length == 0) $state.complete()
else {
this.$store.commit(
"modules/posts/updateLastVisible",
documentSnapshots.docs[documentSnapshots.docs.length - 1].data()
.timestamp
)
}
documentSnapshots.forEach(doc => {
var post = doc.data()
post.docID = doc.id
this.$store.commit("modules/posts/pushPost", post)
})
$state.loaded()
//set completed loading
this.isDocSnapShotLoading = false;
})
}
On the fetch part
async fetch({ store }){
if (store.state.modules.posts.posts.length < 5) {
//check if currently loading
this.$nextTick(()=>{
if (this.isDocSnapShotLoading){
return;
}
});
//set as currently loading
this.isDocSnapShotLoading = true;
let posts = []
await db
.collection("posts")
.orderBy("timestamp", "desc")
.limit(3)
.get()
.then(querySnapshot => {
store.commit(
"modules/posts/updateLastVisible",
querySnapshot.docs[2].data().timestamp
)
querySnapshot.forEach(doc => {
var x = doc.data()
x.docID = doc.id
posts.push(x)
})
//set as completed loading.
this.isDocSnapShotLoading = false;
})
store.commit("modules/posts/fetchedPosts", posts)
}
}

If you wanto be ignore first 3 posts in infiniteHandler then, you can make one post array where you store post id and check whether post id is already loaded or not. I know this should be solved using query but as temporary solution I hope it will work for you.
infiniteHandler($state) {
var next = db
.collection("posts")
.orderBy("timestamp", "desc")
.startAfter(this.lastVisible)
.limit(3)
next.get().then(documentSnapshots => {
//Get the last visible document
// this.lastVisible =
// documentSnapshots.docs[documentSnapshots.docs.length - 1]
if (documentSnapshots.docs.length == 0) $state.complete()
else {
this.$store.commit(
"modules/posts/updateLastVisible",
documentSnapshots.docs[documentSnapshots.docs.length - 1].data()
.timestamp
)
}
documentSnapshots.forEach(doc => {
var check = this.postIdArray.indexOf(doc.id);
if(check == -1){
var post = doc.data()
post.docID = doc.id
this.$store.commit("modules/posts/pushPost", post);
this.postIdArray[] = doc.id;
}
})
$state.loaded()
})
}
async fetch({ store }){
this.postIdArray = [];
if (store.state.modules.posts.posts.length < 5) {
let posts = []
await db
.collection("posts")
.orderBy("timestamp", "desc")
.limit(3)
.get()
.then(querySnapshot => {
store.commit(
"modules/posts/updateLastVisible",
querySnapshot.docs[2].data().timestamp
)
querySnapshot.forEach(doc => {
var x = doc.data()
x.docID = doc.id
this.postIdArray[] = doc.id;
posts.push(x)
})
})
store.commit("modules/posts/fetchedPosts", posts)
}
}

Ok so I found a temporary solution which works for now but is still not pretty:
documentSnapshots.forEach(doc => {
if (
doc.id !== this.posts[0].docID &&
doc.id !== this.posts[1].docID &&
doc.id !== this.posts[2].docID
) {
var post = doc.data()
post.docID = doc.id
this.$store.commit("modules/posts/pushPost", post)
}
})
I also try to make this more efficient with different solutions, thanks so far for your help.

Related

Array.map() doesn't render anything in React

I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);

How to use documents and collections Firebase

Im new to firebase and I need help understanding how to work with documents and collections. Specifically I want to be able to write to a document and let it hold an array of 'loan objects' and let it be specific per user. All of these documents will be held in a collection. This code is making multiple documents for one user and I want it to only make one document per user and if I want to add more data for that user I just want to add it to the existing document
const loansRef = firebase.firestore().collection('goals')
useEffect(() => {
getPW()
let isMounted = true;
if (isMounted) {
loansRef.where('authorID', '==', userId).orderBy('createdAt', 'desc').onSnapshot(
(querySnapshot) => {
const newGoals = [];
querySnapshot.forEach((doc) => {
const goal = doc.data();
goal.id = doc.id + goalCounter.toString();
newGoals.push(goal);
});
console.log('new Goals: '+ newGoals)
console.log('old goals: '+ oldGoals)
// this is my attempt to try to make all loans appear in one array
var oldGoals = courseGoals
for(let j =0; j < newGoals.length; j++){
oldGoals.push(newGoals[j])
}
setCourseGoals(oldGoals);
setGoalCounter(goalCounter+1)
},
(error) => {
console.log(error);
}
);
}
return () => {
isMounted = false;
};
}, []);

Firebase firestore Pagination is returning duplicate results

I'm creating a social feed where I want to have infinite scrolling, using firebase pagination but the query is returning the same each time even when I have a lot of different data in my firestore database.
This is my initial query:
const getThreads = async () => {
try {
setLoading(true);
const ref = firestore()
.collection('Discover')
.orderBy('rank', 'desc')
.limit(10);
let docSnaps = await ref.get();
if (docSnaps.empty !== true) {
let docData = docSnaps.docs.map(document => {
return {
data: document.data(),
id: document.id,
};
});
setLastVisible(docData[docData.length - 1].id);
setThreads(docData); //Setting the data to display in UI
}
setLoading(false);
} catch (e) {
setLoading(false);
console.log(e);
Alert.alert(
'Oops! Looks like something went wrong',
'Please try again later',
[{text: 'OK'}],
);
}
};
As you can see each post/thread is being ordered by a rank field. And I'm setting the last visible as the documentId which is being used in the below query to get more posts/threads
async function getMoreThreads() {
try {
console.log('Getting More threads');
if (lastVisible !== null) {
setRefreshing(true);
const ref = firestore()
.collection('Discover')
.orderBy('rank', 'desc')
.startAfter(lastVisible)
.limit(10);
let docSnaps = await ref.get();
if (docSnaps.empty !== true) {
let docData = docSnaps.docs.map(document => {
return {
data: document.data(),
id: document.id,
};
});
console.log('DocData', docData.length);
setLastVisible(docData[docData.length - 1].id);
setThreads([...threads, ...docData]);
}
setRefreshing(false);
}
} catch (e) {
console.log('Error getting more', e);
Alert.alert(
'Oops! Looks like somthing went wrong',
'Please try again later',
[{text: 'OK'}],
);
}
}
My hypothesis of why this is happening is because I'm using documentIds to paginate and my document Ids are numeric long integer strings like this
1002103360646823936,1259597720752291841, 974895869194571776, etc.
Help would be very much appreciated.
Your hypothesis is correct. The field that you are using to paginate with startAfter(...) should match the field you are using in the orderBy(...) method - in this case, your startAfter(...) method is assuming that you are passing it a rank value.
You can pass in the DocumentSnapshot object in your startAfter(...) method instead:
const ref = firestore()
.collection('Discover')
.orderBy('rank', 'desc')
.startAfter(documentSnapshot) // document snapshot of the last element
.limit(10);

Vuetify data table doesn't get the updated array values

I populating the Vuetify data table with some user data. When I remove a user from the data table I update the user array like this:
handleDelete(user) {
confirm("Are you sure you want to delete this user?") &&
axios
.delete("user/" + user.id)
.then(response => {
// Delete user from user array
this.users = this.users.filter(function(el) {
return el.id != user.id;
});
})
},
When I register a new user the array is also updated but now like this:
handleRegister(user) {
axios
.post("user/register", user)
.then(response => {
// Update User array
this.users.push(response.data.user);
})
},
This all works fine, except when I update a user. In my function I search for the user object in the array of users and replace it for the updated one. But somehow the data table don't get updated with the new values. The update function looks like this:
handleUpdate(user) {
const id = user.id;
axios
.put("user/" + id, user)
.then(response => {
// TODO: Update User array
let foundIndex = this.users.findIndex(
x => x.id == response.data.user.id
);
this.users[foundIndex] = response.data.user;
})
},
When I console.log the values of this.users[foundIndex] and response.data.user it shows me the right values. But somehow it seems like the data table doesn't get updated.
i think its problem with scope of "this" variable.
// it should be like this below
handleDelete(user) {
const _this_0 = this;
confirm("Are you sure you want to delete this user?") &&
axios
.delete("user/" + user.id)
.then(response => {
// Delete user from user array
_this_0.users = _this_0.users.filter(function(el) {
return el.id != user.id;
});
})
}
handleRegister(user) {
const _this_0 = this;
axios
.post("user/register", user)
.then(response => {
// Update User array
_this_0.users.push(response.data.user);
})
},
handleUpdate(user) {
const id = user.id;
const _this_0 = this;
axios
.put("user/" + id, user)
.then(response => {
// TODO: Update User array
let foundIndex = _this_0.users.findIndex(
x => x.id == response.data.user.id
);
_this_0.users[foundIndex] = response.data.user;
})
}
I currently fixed it by writing this function:
updateUserArray(user) {
// Look for the index of the user
let index = this.users.findIndex(
x => x.id == user.id
);
// Remove the user from the array
this.users= this.users.filter(function(el) {
return el.id != user.id
});
// Add the updated value to the array
this.users.splice(index, 0, user);
},
Seems like I had to use the splice function because this forces the DOM to refresh while directly updating the array doesn't.

firestore < query mixed with orderBy Query

I am using firestore to query documents with compound queries, my code is:
let query = firestore.collection( 'market' )
let res = []
let newChkPt = false
// states
query = query.where('deListTime', '==', false)
query = query.where('tradeTime' , '==', false)
query = query.where('expirationTime', '>', Date.now())
// FIFO ordering
query = query.orderBy('originationTime', 'desc')
query = query.limit(3)
if (chkPt) {
await query
.startAfter(chkPt)
.get()
.then(snap => {
snap.forEach(doc => {
res.push(doc.data());
newChkPt = doc.data()['originationTime']
})
})
.catch(e => { console.log(e); return false})
} else {
await query
.get()
.then(snap => {
snap.forEach(doc => {
res.push(doc.data());
newChkPt = doc.data()['originationTime']
})
})
.catch(e => { console.log(e); return false})
}
In the console I have every combination composite query indices possible specified amongst the fields deListTime, tradeTime, expirationTime, and originationTime. And yet this compound query I specified refuse to fetch data as intended. If I comment out
query = query.orderBy('originationTime', 'desc')
I get the data, and if I comment the '>' out whilst leaving everything else un-commmented:
query = query.where('expirationTime', '>', now)
I also get the desired data. Is it the > that's messing it up?
The indexes:
Are you initializing 'now' somewhere? Did you mean Date.now()?

Categories

Resources