Binding VuexFire to a collection filtered with a query - firebase

I'm unsuccessfully trying to bind a Vuex state attribute to a queried collection in FireStore. I was wondering if anyone with more experience could point me in the right direction. This is what I'm currently doing:
In a Vuex Module called auth I'm declaring the following bind to userArticles
export const bindUserArticles = firestoreAction(({ bindFirestoreRef }, id) => {
return bindFirestoreRef('userArticles', userCollectionRef('articles', id))
})
This in turn points to a firebase method for querying the data (which works)
export const userCollectionRef = (collectionName, id) => {
return firestore().collection(collectionName).where("author.idAuthor", "==", id)
}
And I'm importing and dispatching the method in my Vue file in the following way
computed: {
...mapGetters('user', ['currentUser']),
},
methods: {
...mapActions('articles', ['bindUserArticles']),
},
watch: {
currentUser () {
this.bindUserArticles(this.currentUser.id)
}
}
So when the currentUser is updated upon login the method is triggered. The method is triggered and the right id is being sent, I've tested it with console.log. There is no error being displayed. When I try for example to modify the idAuthor of an existing article in the database, the list userArticles does not update. When I try adding or deleting an article from the database that has the specific idAuthor, the list userArticles does not update. I've also tried placing the this.bindUserArticles(this.currentUser.id) in the created() and mounted() life-cycle, to no avail.Does anyone have a clue where I'm going wrong about this?
Thanks in advance

Related

Data function not working react native firestore

I got stuck in this puzzle which doesn't seem to wanna be solved, I am kinda sure I am forgetting something since I just started learning react-native.
I have this code :
async componentDidMount() {
let user = await UserRepository.getUserRef(firebase.auth().currentUser.uid);
await firebase
.firestore()
.collection("reminder")
.where("user", "==", user)
.get()
.then((remindersRecord) => {
remindersRecord.forEach((reminderDoc) => {
console.log(reminderDoc.data());
});
});
I am trying to get the "reminders" data of the connected user, the query works since we got reminderDoc which contain a bunch of objects, and inside there is the data I want but when I call data() nothing changes, I don't get the document it returns the same object.
Reminder collection :
Any help is much appreciated!
I tried to replicate this on my side and I think this is working fine. I think that result that you get is related with fields boss and user which I guess are reference type in firestore. If you log to console such fields give results like this:
{
reference: DocumentReference {
_firestore: Firestore {
_settings: [Object],
_settingsFrozen: true,
_serializer: [Serializer],
_projectId: <PROJECT_ID>,
registeredListenersCount: 0,
bulkWritersCount: 0,
_backoffSettings: [Object],
_clientPool: [ClientPool]
},
_path: ResourcePath { segments: [Array] },
_converter: {
toFirestore: [Function: toFirestore],
fromFirestore: [Function: fromFirestore]
}
},
text_field: 'test',
...
}
So for presented example you will get 2 such fields and for those fields you will not see as a string. BTW the timestamp field will not be shown properly as well.
To avoid this issue you can use example path property of document reference or when it comes to timestamp you can use toDate() method. I have created small example to show the fields properly (looping over all the object fields):
remindersRecord.forEach((reminderDoc) => {
for (const [key, value] of Object.entries(reminderDoc.data())) {
if (key == 'boss' || key == 'user') console.log(`${key}: ${value.path}`)
else if (key == 'startAt') console.log(`${key}: ${value.toDate()}`)
else console.log(`${key}: ${value}`)
});
I tested this in nodejs directly, but it should work in componentDidMount as well.

Nuxt this.$route is undefined

I have a dynamic route called products/_id which should load an product from Firestore. I am using VueFire to fetch the product from the Firebase database. Fetching the whole products collection works perfectly fine.
firestore: {
product: firebase
.firestore()
.collection("products")
}
But getting the document with a certain id is where the trouble begins. Normally I would call
firestore: {
product: firebase
.firestore()
.collection("products")
.doc(this.$route.params.id),
}
But now I get an error saying "TypeError: Cannot read property '$route' of undefined". Which is strange because if i log the route to the console I can actually see the id. Moreover, calling ".doc("DOCUMENT_ID") work perfectly fine.
Anyone knows how to fix this? I've tried a combination of methods and mounted hooks to retrieve the id but to no avail.
mounted() {
this.getProduct();
},
methods: {
getProductId() {
return this.$route.params.id;
},
},
firestore: {
products: firebase.firestore().collection("products").doc(this.getProductId),
}
Brings up the same error...
firestore is specified as an object here, so it's evaluated immediately outside the context of the component, so this would not be the Vue component instance.
To resolve this, specify firestore as a function that returns the intended object, as shown in the docs:
export default {
firestore() {
// ✅ `this` is Vue component instance inside function
return {
products: firebase.firestore().collection("products").doc(this.getProductId),
}
}
}

Decrease response time in Firebase Vue app when liking a post

I have an app with different 'procedures' (think posts or pages), which one can like. Currently the process works: Tap like => run method "likeProcedure" => run dispatch action "likeProcedure" => update UI. It usually happens almost immediately, but sometimes there's a lag that gives this a "non-native" feel. Is there some sort of way that I could return feedback immediately, while stile holding single origin of truth on the firebase database?
Thank you!
Page Code:
<v-icon
v-if="!userProfile.likedProcedures || !userProfile.likedProcedures[procedure.id]"
color="grey lighten-1"
#click="likeProcedure({ id: procedure.id })"
>
mdi-star-outline
</v-icon>
and
computed: {
...mapState(["userProfile"]),
procedures() {
return this.$store.getters.getFilteredProcedures();
},
},
Vuex code:
async likeProcedure({ dispatch }, postId) {
const userId = fb.auth.currentUser.uid;
// update user object
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: true,
});
dispatch("fetchUserProfile", { uid: userId });
},
Side note: I'm trying to remove the dispatch("fetchUserProfile") command, but this doesn't work, because then I'm calling dispatch without using it. And I cannot remove dispatch because then the object calling it is empty. And I cannot remove the object, because then the argument ('postId') isn't working. So if anyone knows how to deal with that, that would be extremely helpful.
Thank you :)
So this is the best solution I've come up yet. It kind of destroys the idea of a single source of truth, but at least it provides an immediate UI update:
async likeProcedure({ dispatch, state }, postId) {
console.log("likeProcedure");
const userId = fb.auth.currentUser.uid;
// line below provides immediate update to state and hence to the UI
state.userProfile.likedProcedures[postId.id] = true;
// line below updates Firebase database
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: state.userProfile.likedProcedures[
postId.id
],
});
// line below then fetches the updated profile from Firebase and updates
// the profile in state. Kind of useless, but ensures that client and
// Firebase are in-sync
dispatch("fetchUserProfile", { uid: userId });
},
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.usersCollection.doc(user.uid).get();
// set user profile in state
commit("setUserProfile", userProfile.data());
// change route to dashboard
if (router.currentRoute.path === "/login") {
router.push("/");
}
},

Vuejs & Firestore - How to Update when Data Changes in Firestore

I've gone through a bunch of tutorials and docs but cannot seem to be able to update on page when data changes in Firestore (NOTE: not Firebase)
Heres what I have currently which is working fine except if data changes in the DB it is not reflected on the page itself unless I refresh. Code below is within script tags:
import { recipeRef } from '../../firebase';
export default {
data() {
return {
recipes: []
}
},
firestore: {
recipes: recipeRef
},
created() {
db.collection('recipes').get().then((onSnapshot) => {
this.loading = false
onSnapshot.forEach((doc) => {
let data = {
'id': doc.id,
'name': doc.data().name
}
this.recipes.push(data)
})
})
}
I'm not using Vuex. Adding data, editing and reading works fine. Just not reflecting changes once data has changed. Maybe there is a life cycle hook Im supposed to be using? For "onSnapshot" - Ive tried "snap", "querySnapshot" etc. No luck.
Thanks in advance.
Remove the get() and just replace with snapshot - like so
created() {
db.collection('recipes').onSnapshot(snap => {
let foo = [];
snap.forEach(doc => {
foo.push({id: doc.id, name: doc.data().name})
});
}
});
I am not familiar with the firestore API, but glancing through the docs, it looks like calling get() is how you query a single time. Where you have onSnapshot should really be querySnapshot -- that is, the results of a one query. See:
https://firebase.google.com/docs/firestore/query-data/get-data
versus:
https://firebase.google.com/docs/firestore/query-data/listen
So to get live updates, it looks like you need to create a listener, like so:
db.collection('recipes')
.onSnapshot(function(snapshot) {
snapshot.forEach(function(doc) {
// Find existing recipe in this.recipes
// and swap in the new data
});
}, function(error) {
// handle errors
});
I think you will need to add that listener in addition to the get() query you are currently doing. Hope this helps!

Vuejs 2, VUEX, data-binding when editing data

I have a user profile section and Im trying to allow the user to edit their information. I am using vuex to store the user profile data and pulling it into the form. The edit form is located in a child component of the userProfile component - which loads the data save commits it to VUEX.
So I can populate the form with the data from VUEX, but as soon as I change any values in the form, it changes the value in my parent component as well.
I am not committing changes to VUEX until the form is saved, so it means the data is bound two way to VUEX. I was under the impression this was not possible. In this case it is not desired since if the user changes some data, then navigates away without actually clicking "save", the data is VUEX is still changed.
Note, this is a simplified example. Im actually using router view to load the child component, or I would pass the data through props. I have tested loading the edit-profile component directly like it is below, and I have the same issue.
Please see the code below, I can't find why the data is being sent back up to the store. Any help is greatly appreciated.
In the parent, I set retrieve the user data like so:
<template>
<div class="content">
<h1>{{getUserDetails.firstname}} {{getUserDetails.lastname}} </h1>
<edit-profile></edit-profile>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import EditProfile from './Edit.vue';
export default {
data() {
return {
// data
}
},
created () {
this.fetchData();
},
components: {
EditProfile:EditProfile
},
computed: mapGetters([
'getUserDetails'
]),
methods: {
fetchData: function () {
var _this = this;
// ajax call - then
_this.$store.commit('setData', {
name: 'userDetails',
data: response.data.data.userDetails
});
}
}
}
</script>
This loads the results and stores them in the store, and works great.
My store has this:
const store = new Vuex.Store({
state: {
userDetails: {}
},
mutations: {
setData(state, payload){
state[payload.name] = payload.data;
}
},
getters: {
getUserDetails: state => {
return state.userDetails;
}
}
}
Everything here is working.
In my child component with the edit form, I am populating the form like this:
<template>
<form>
<label>Name</label>
<input name="firstname" v-model="profile.firstname">
<input name="lastname" v-model="profile.lastname">
<button v-on:click="save">submit</button>
</form>
</template>
<script>
import {mapGetters } from 'vuex';
export default {
data() {
return {
profile:{}
}
},
watch: {
getUserDetails (newData){
this.profile = newData;
}
},
created (){
this.profile = this.$store.getters.getUserDetails;
},
computed: mapGetters([
'getUserDetails'
]),
methods:{
save (){
var _this = this;
// ajax call to POST this.profile then
_this.$store.commit('setData', {
name: 'userDetails',
data: this.profile
});
}
}
}
</script>
If you are looking for a non binding solution with vuex you can clone the object and use the local version for v-model than on submit commit it.
in your created lifecycle function do this:
created (){
this.profile = Object.assign({}, this.$store.getters.getUserDetails);
},
Why I think it is not working as expected for you: you're receiving an object, binding it to a local property. Then when you change that local property, it's bound by object pointer (/memory address) to the store's object.
Creating a new object and setting the properties on that new object based on the properties of the state's user profile object should do the trick, since the new object would have it's own address in memory, would point to another place...
Illustration:
created (){
// create a new object with {...}
this.profile = {
// assign values to properties of same name
firstName: this.$store.getters.getUserDetails.firstName,
lastName: this.$store.getters.getUserDetails.lastName,
};
},
However if those properties (firstName, lastName) are objects or arrays (anything accessed by pointer to memory address) then this wouldn't work either.
So... what I'd most likely end up doing myself in this situation is something like this:
data() {
return {
firstName: '',
lastName: ''
}
},
This defines local properties. When loading the data, you would populate the local values with profile data you have in the Vuex store.
created() {
let profile = this.$store.getters.getUserDetails;
if (profile.firstName && profile.lastName) {
this.firstName = profile.firstName;
this.lastName = profile.lastName;
}
},
Then, when saving, you use your local variables to update the store's values.
methods: {
save() {
let profile = {
firstName: this.firstName,
lastName: this.lastName
};
// ajax call to POST this.profile then
this.$store.commit('setData', {
name: 'userDetails',
data: profile
});
}
}
I'm writing this from the top of my head, so there might be a bug or typo in here... But I hope at the very least my logic is correct ;-P and clear to you.
Pro: until you're ready to save the edited information, you're not reflecting it anywhere else.
Con: if you'd need to reflect temporary changes (maybe in a User Profile Preview area), this might or might not work depending on your app's structure. You might want to bind or save on #input to a state.temporaryUserProfile object in that case?
I am still new to Vue.js, started using it 2 weeks ago. Hope this is clear and correct :)
The problem is caused by using v-model with mapGetters - this creates the two-way binding you've described. The simple solution is to use value instead:
:value="profile.firstName"
This way the form is only changing the local copy of field and not pushing the changes back to the Vuex store.
#AfikDeri solution is great, but it only create a shallow copy(for example it wont work if you have nested objects, which is common to have), to solve this you may serialize then parse your vuex state object getUserDetails, as follow:
created (){
this.profile = JSON.parse(JSON.stringify(this.$store.getters.getUserDetails));
}

Resources