How do I structure a fetch request after an other? - reactjs

I have a React blog and I am using Firebase as a back-end. I use createUserWithEmailAndPassword method and after authentication users are redirected to blog where they can start adding blogs.
I store their posts in a firestore collection "posts". No problem with that.
I also want a user object to be created after login with the user preferences. Let's say a specific theme each one has chosen.
I create a collection called "users" at firestore and where I will store each ones object {user: "random#hotmail.com, theme: "dark" ,isAdmin: false, etc} using addDoc method.
I want this object to be created once though and not every time a user logs in.
How do the check on that "users" collection if the user already exists?
I get the collection getDocs(userCollectionRef) and then I filter the data let's say by a property to see if there is that object there.
And if not I want to add the document using addDoc method.
this is the request:
useEffect(() => {
const createUserData = async () => {
const data = await getDocs(usersCollectionRef);
const docs = data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
const userData = docs.filter((doc) => doc.user === user.email);
if (userData.length === 0) {
await addDoc(usersCollectionRef, {
user: user.email,
isAdmin: false,
theme: "dark",
});
}
};
if (user) {
createUserData();
}
}, [user, usersCollectionRef]);
It seems like I am trying
to check and add to the collection at the same time and this is why it doesn't work.
Any ideas?
Should I have an in-between step where I store what I'm getting from the getDocs in a state or something and then do the second request?
Can anyone explain please?

I changed the if statement to a return like so and it worked.
return (
userData.length === 0 &&
(await addDoc(usersCollectionRef, {
user: user.email,
wordGoal: wordGoal,
}))
);
I guess I return the second promise after the first now that's why

I guess when the getDocs request happens because its asynchronous the data then are empty then the code proceeds to the if statement which at that point is true (we have no records) and so executes it.

Related

Writing to Firebase Firestore React

so I have a problem right now. I'm entering the users dates into cloud firestore like this:
so this is a user collection, with a document by user's id's and then the dates are entered as a list. But whenever I refresh the page and enter new data, all the previous data disappears.
So I'm wondering how do I enter data so that it goes like collection(userCalendar).doc(USERID).collection(dates) and then it has all the user's data entered as strings rather than an array like I've been doing.
My code for the way it's behaving right now is below. Thank you! :)
export const allEvents = [];
const Calendar = () => {
const [date, setData] = useState([]);
const handleDateClick = async (DateClickArg) => {
if (DateClickArg.jsEvent.altKey) {
const title = prompt("Enter title", DateClickArg.dateStr); // allows user to put a title in
// making object
const event = {
title: title ? title : DateClickArg.dateStr,
start: DateClickArg.date,
allDay: true
}
allEvents.push(event)
const db = fire.firestore();
let currentUserUID = fire.auth().currentUser.uid
const doc = await fire
.firestore()
.collection("userCalendar")
.doc(currentUserUID)
.get()
db.collection("userCalendar")
.doc(currentUserUID)
.set({
activites: allEvents
})
}
}
You can use arrayUnion() to add new items to an array however it'll be difficult for you to query activities of a user.
For example, you cannot fetch a single activity from that array but you'll have to fetch all of them get the required one. Additionally, you cannot update an object in an array directly in Firestore.
Also a document has a max size limit of 1 MB so if a user can have many activities, it'll be best to create sub-collection.
I would recommend restructuring the following way:
users -> { userId } -> activities-> { activityId }
(col) (doc) (col) (doc)
All of user's activities/events are now in a sub-collection "activities" and each activity would be a document instead of an array item. With this you can easily read/update/delete a single activity.
Also checkout: Firestore subcollection vs array
Not sure whether this meets your requirement, but from my understanding you just want to update the activities with the allEvents which contains all the updated activities.
db.collection("userCalendar")
.doc(currentUserUID)
.set({
activites: allEvents
})
should become
db.collection("userCalendar")
.doc(currentUserUID)
.set({
activites: allEvents
}, { merge: true })
Or you can use the update method
db.collection("userCalendar")
.doc(currentUserUID)
.update({
activites: allEvents
})
From the docs
To update some fields of a document without overwriting the entire document, use the update() method:
import { doc, setDoc } from "firebase/firestore";
const cityRef = doc(db, 'cities', 'BJ');
setDoc(cityRef, { capital: true }, { merge: true });
It looks like you're overwriting your collection with every code execution:
db.collection("userCalendar")
.doc(currentUserUID)
.set({
activites: allEvents
})
You should consider to make an array union, so that the values are added to your collection instead of overwriting them:
db.collection("userCalendar")
.doc(currentUserUID)
.update({
activites: firebase.firestore.FieldValue.arrayUnion(
{
allEvents
}),
})
Also some examples from firestore docu:
https://cloud.google.com/firestore/docs/samples/firestore-data-set-array-operations

React Redux FireStore - Changes in FireStore collection does not reflect on my screen unless I refresh

I use React with Redux and Firebase. Here is one of the functions from my Action.js
export const loadItemsInCategory = (categoryId) => {
return (dispatch) => {
let itemsArray = [];
firestoreService.getItemsInCategory(categoryId)
.then(updatedGroceryList => {
itemsArray = updatedGroceryList;
console.log(`category id is ${categoryId}`)
dispatch(loadItemsInCategoryHelper(categoryId, itemsArray))
})
.catch((error) => console.log(error));
}
}
It's a normal FireStore query. Here is what happens in firestoreService.getItemsInCategory(categoryId)
export const getItemsInCategory = async (categoryId) => {
console.log(`firebase category id is ${categoryId}`)
const snapshot = await db.collection('Item').where('category', '==', categoryId).get()
return snapshot.docs.map(doc => {console.log("called");return {id: doc.id, ...doc.data()}});
}
Right now, my application shows the list of items in the given Category. However, the list does not get updated when a new Item is added to the category by someone else. In other words, additions in FireStore collection does not reflect on my screen unless I refresh the page.
How can I code my webapp in such a way that any change on the FireStore end gets reflected on my webapp?
Thanks in advance!
Your code is doing a one-time query with get(). Queries made like this are not realtime. They don't refresh.
If you want to receive updates to your query in realtime, you should follow the documentation for realtime queries. Instead of using get(), you will use onSnapshot(). And instead of getting a promise, you will attach a listener callback that will be invoked whenever there is a change in the results of the query. Because of these differences, your code will look drastically different.

Lookup multiple Firebase entries using Redux action

I have this Redux action, which looks up a user and returns all the items they have:
export const itemsFetch = () => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items`)
.on('value', snapshot => {
dispatch({ type: ITEMS_FETCH_SUCCESS, payload: snapshot.val() });
});
};
};
Works great, and each item returned has a unique key associated with it.
I want to modify this action to look up specific items, which I've done. That works fine too:
export const itemLookup = uid => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${uid}`)
.on('value', snapshot => {
dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: snapshot.val() });
});
};
};
This also works fine, but I can only use this to lookup a single item.
I want to loop over an array of item ids, and lookup details for each. Doing this from a component, and using mapStateToProps, causes the component to rerender each time, losing the previous lookup in the process.
Is it best to loop over the ids I have at a component level, and make multiple calls. Or should I pass the array to the action, and somehow loop over them within the action?
Thanks
I feel like I'm doing something dumb, or misunderstanding Redux completely.
In my opinion, this is one of the few limitations that firebase has (along side with queries) that sometimes make me want to grow hair again and lose it (I am bald).
I am more experienced with Firestore although I have used Database, but I think you are correct that you can only request one item in Firebase. What I would do to solve this, is to create a new action that receives an array of IDs and then executes and array of promises that will query each doc.
Something like (pseudo code, and you might need to wrap your firebase call into a promise):
let promises = [];
arrayIds.forEach(id => {
promises.push(firebase.database().ref.get(id))
})
return Promise.all(promises).then(dispatch(results))
Now, if you find that the amount of results are usually not a lot, it is totally fine (and usually the way Firebase requires you to) to complete the data filtering in the client.
Using the response from sfratini, I managed to work it out. Here's the action:
export const itemLookup = oid => {
const { currentUser } = firebase.auth();
const items = []
return dispatch => {
new Promise(function (res, rej) {
oid.forEach(id => {
firebase.database().ref(`users/${currentUser.uid}/items/${id}`).on('value', snapshot => {
items.push(snapshot.val())
})
})
}).then(dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: items }))
}
I used the same reducer, and now the items array makes it down to component level. Perfect!

Update a collection with existing values in firebase

I'm developing a react web application with firebase and Now I'm stuck with this problem. So what I want is update the collection with existing data. For example let's say that the following details are already in the collection
org_details: {
general: {
org_name: "ane",
founder: "fng"
},
group:{
admin:{
<details of the admin grp>
},
standard:{
<grp details>
}
}
}
So what I want is add another group details with the existing groups. For an example I need to add a group called "fun" by also having admin and standard group. So this is the firebase query that I've tried.
export const createGroup = (data, history) => async (
dispatch,
getState,
{ getFirestore }
) => {
const firestore = getFirestore();
const { email: userEmail } = getState().firebase.auth;
const groupName = data.groupName;
dispatch({ type: actions.CREATE_GROUP_START });
try {
firestore
.collection("org")
.doc(userEmail)
.update({
"group": {
groupName: {
org_grp_admin: data.org_grp_admin,
org_grp_users: data.org_grp_users
}
}
})
.then(() => {
getOrgData(dispatch, getState, { getFirestore });
history.push("/groupmanagement");
});
dispatch({ type: actions.PROFILE_EDIT_SUCCESS });
} catch (err) {
dispatch({ type: actions.CREATE_GROUP_FAILS, payload: err.message });
}
};
But this query doesn't seem to behave like that I want. It always create a group called "groupName" instead of the group name that is passed from the parameter and always replace the existing data. How should I change the query to get the result that I want?
And I'm using firestore in firebase as the database.
As you are trying to dynamically assign the property key you need to wrap it in [groupName]:{...} to pick up the variable groupName instead of just the string 'groupName'
"group": {
[groupName]: {
org_grp_admin: data.org_grp_admin,
org_grp_users: data.org_grp_users
}
}
As for performing a deep merge (e.g. just update the subgroup if it exists, and if not create the new group without deleting others), this is not possible with the current api however you could either
option 1) Read the data from the database first yourself and manually handle the merge before writing (you could either write your own function or use a package like deepmerge
option 2) Restructure your data to be flatter, for example using a subcollection to store your groups

What's the best way to check if an element exists in a firestore array, and if it does, run a function, and if not add it?

I'm developing a 'like' button and currently the likes increment up by 1 each time the like button is clicked. However, I want to make this more robust and dynamic, and if a logged in user has already liked a post, then I want their 'like' to go away ala Facebook, Reddit (upvote), etc.
Currently what I'm doing is keeping track of the number of likes and who has liked a post. The structure looks like this:
post: {
likes: 3,
likedBy: ['userA', 'userB', 'userC']
}
So what I want to happen is: when the like button is clicked, I want to search the likedBy property to see if the logged in user has already liked the post, and then either increment liked by 1, and add them to the array, or decrement likes by 1 and remove them from the array.
I'm having a hard time figuring out how to write this logic with the React action that handles this interaction with firestore.
Here is what I have written so far:
export const newLike = (post) => {
return (dispatch, getState, {getFirebase, getFirestore}) => {
const firestore = getFirestore();
const signedInUser = getState().firebase.auth;
console.log(signedInUser)
firestore.collection('posts').doc(post.id).update({
likes: (post.likes + 1),
likedBy: firestore.FieldValue.arrayUnion(signedInUser)
}).then(()=> {
dispatch({type: types.NEW_LIKE, post});
}).catch((err) => {
dispatch({ type: types.NEW_LIKE_ERROR, err});
});
};
};
In your data structure, you don't need to keep likes, as you can get it from likedBy. I'm not familiar with firestore api, but logic below should do what you want, you just need to get/set date to/from firestore
return (dispatch, getState, {getFirebase, getFirestore}) => {
const signedInUser = getState().firebase.auth;
const posts = ...// get current state of post, with likedBy field. Also, I assume here, that likedBy is always an array, with data or empty
let updatedLikedBy;
if(posts.likedBy.indexOf(signedInUser) ===-1){ //assuming that signedInUser is a string
updatedLikedBy = [...post.likedBy, signedInUser]
} else {
updatedLikedBy = post.likedBy.filter(item=>item!==signedInUser)
}
const updatedPost = {likedBy: updatedLikedBy} // now you can send this to firestore, and fire actions after success or fail.
};
};
To get likes now you just need to do posts.likedBy.length

Resources