Vuex, changing state in an action - firebase

I'm trying to code as "good practice" as possible while learning Vuex.
From what I learning I tought that Actions are used to do for example external API calls, the result of that is passed to a Mutation via a commit().
Now I want to increment a counter for a certain user on Firebase. This is working when I code my action like this
ADD_CREDIT(context, user) {
user.credits++;
firebase.database().ref('users').child(user.id)
.update({credits: user.credits})
.then(() => {});
}
So in my action I already update the state before calling the API call. Is this good practice? I tried it the other way using the following code, but that just looks to complicated.. And it doesn't work for now.
Action
ADD_CREDIT({commit, state}, user) {
const newcredits = user.credits + 1;
firebase.database().ref('users').child(user.id)
.update({credits: newcredits})
.then(() => {
commit('CREDIT_CHANGED', user.id, newcredits)
});
}
Mutation
CREDIT_CHANGED(state, userid, newcredits) {
let user = state.users.find(user => {
return user.id = userid
});
user.credits = newcredits;
}

The pattern of a mutation function is
function mutation(state, payload) {
...
// do something with state
state.person = payload;
...
}
It doesn't have anymore argument than that 2.
So, your mutations should pass an object with all of your information. Like this:
CREDIT_CHANGED(state, payload) {
let user = state.users.find(user => user.id === payload.userid);
user.credits = payload.newcredits;
}
And then you action should commit like this:
ADD_CREDIT({commit, state}, user) {
const newcredits = user.credits + 1;
firebase.database().ref('users').child(user.id)
.update({credits: newcredits})
.then(() => {
commit('CREDIT_CHANGED', {
userid: user.id,
newcredits: newcredits
})
});
}

Related

Firebase Firestore returns a promise in Vue

I'm trying to use some data from from Firestore. before it used to work, now in Vuetify I keep getting 'PENDING' if I try to access the $data.users
export default {
data() {
return {
users: [],
};
},
created() {
db.collection('users').get().then((snapshot) => {
snapshot.forEach((doc) => {
const user = doc.data();
user.id = doc.id;
this.users = user;
console.log(user.documents.selfie.url); // Here the log return the value correctly
});
});
},
methods: {
imageUrl(user) {
console.log(user.documents.selfie.url); // Here the log return "Pending";
},
Inside the template I run a v-for (user, index) in users :key='index'
ERROR:
Uncaught (in promise) TypeError: Cannot read property 'selfie' of undefined
It's difficult to be 100% sure without reproducing your problem, but I think the problem comes from the fact that the Promise returned by the asynchronous get() method is not yet fulfilled when you call the imageUrl() method. This is why you get the pending value.
One possibility to solve that is to check as follows:
methods: {
imageUrl(user) {
if (user) {
console.log(user.documents.selfie.url);
} else {
//...
}
},
Also, is seems you want to populate the users Array with the docs from the users collection. You should do as follows:
created() {
db.collection('users').get().then((snapshot) => {
let usersArray = [];
snapshot.forEach((doc) => {
const user = doc.data();
user.id = doc.id;
usersArray.push(user);
console.log(user.documents.selfie.url); // Here the log return the value correctly
});
this.users = usersArray;
});
},
With your current code you assign the last user in the loop, not the list of users.

Get data from Firebase in Vue.js component

I'm starting my first serious app with Vue.js and I have an issue gathering data from Firabase. The idea here is simply to get data linked to an user ID. My first though was to store that in a computed value, like so
export default {
...
computed: {
userInfo: function() {
const firestore = firebase.firestore();
const docPath = firestore.doc('/users/' + firebase.auth().currentUser.uid);
docPath.get().then((doc) => {
if (doc && doc.exists) {
return doc.data();
}
});
}
}
}
But, when I try to access this variable, it's undifined.
My guess is that the value is computed before the asynchronous call has ended. But I can't see how to get around it.
Indeed you have to take into account the asynchronous character of the get() method. One classical way is to query the database in the created hook, as follows:
export default {
data() {
return {
userInfo: null,
};
},
....
created() {
const firestore = firebase.firestore();
const docPath = firestore.doc('/users/' + firebase.auth().currentUser.uid);
docPath.get().then((doc) => {
if (doc && doc.exists) {
this.userInfo = doc.data();
}
});
}
}

How do I get firestore collection reference from within then() function?

I am using vuexfire to bind firebase references to my app state.
This works fine:
bindRef: firebaseAction(({bindFirebaseRef}, payload) => {
let firebaseRef = db.collection(`/${payload}`)
bindFirebaseRef('storeProperty',firebaseRef)
})
I, however, only want to do the binding after a successful get; just so that I can be able to catch errors and also set progress indication.
Something like this:
bindRef: firebaseAction(({bindFirebaseRef}, payload) => {
let firebaseRef = db.collection(`/${payload}`).get().then(e => {
//where ref is same as firebaseRef
bindFirebaseRef('questions',ref)
})
})
You need to declare the reference to that collection as a variable and only then you can pass it on to your function:
bindRef: firebaseAction(({bindFirebaseRef}, payload) => {
let firebaseRef = db.collection(`/${payload}`)
firebaseRef.get().then(e => {
//pass firebaseRef to the function
bindFirebaseRef('questions',firebaseRef)
})
})

correct way to use firestore onSnapShot with react redux

I'm trying to figure out the correct way to use firestore.onSnapshot with react-redux.
I currently have this code in my action file, which I am calling on componentWillMount() in my component.
export const fetchCheckins = () => async (dispatch) => {
const {currentUser} = firebaseService.auth();
try {
let timestamp = (new Date());
//set timestamp for beginning of today
timestamp.setHours(0);
//get checkins today
let checkinstoday = (await firebaseService.firestore().collection(`/checkins/${currentUser.uid}/log`).where("timestamp",">=",timestamp).orderBy("timestamp","desc").get()).docs.map(doc => doc.data());
//set timestamp for beggining of week
timestamp.setDate(-(timestamp.getDay()));
//get checkins (week)
let checkinsweek = (await firebaseService.firestore().collection(`/checkins/${currentUser.uid}/log`).where("timestamp",">=",timestamp).orderBy("timestamp","desc").get()).docs.map(doc => doc.data());
//set timestamp for begging of month
timestamp.setDate(0);
//get checkins (month)
let checkinsmonth = (await firebaseService.firestore().collection(`/checkins/${currentUser.uid}/log`).where("timestamp",">=",timestamp).orderBy("timestamp","desc").get()).docs.map(doc => doc.data());
dispatch({type: FETCH_CHECKINS, payload: { today: checkinstoday, week: checkinsweek, month: checkinsmonth}});
}
catch(e){
console.error(e);
}
};
this works fine, the correct data is sent to the component and display. The problem is, that if the user checks in, the checkin data should adjust, but it does not, since I am getting the data once and sending it, and the state is not re-rendering.
My question is how I should approach this? Do I use .onSnapshot() instead of .get()? Do I call .fetchCheckins() from the .checkin() action creator? How do I approach according to best practice? thank you
According to firestore's documentation if you need realtime updates you should use onSnapshot:
https://firebase.google.com/docs/firestore/query-data/listen
In your case if you use .get() - you get the update once and firestore won't notify you if any of the data changes. That's why you are not seeing the changes.
P.S. checkout redux-firestore: https://github.com/prescottprue/redux-firestore - it's nice library that can help you with your redux bindings.
You could subscribe your list like this:
function subscribeToExperiences() {
return eventChannel((emmiter: any) => {
experiencesRef.onSnapshot({ includeMetadataChanges: true }, snapshot => {
const experiences: IExperience[] = snapshot.docChanges().map(change => ({
id: change.doc.id,
title: change.doc.data().title
}));
if (snapshot.docChanges().length !== 0) {
emmiter(experiences);
}
});
return () => experiencesRef;
});
}
function* fetchExperiences(_: ExperiencesFetchRequested) {
const channel = yield call(subscribeToExperiences);
try {
while (true) {
const experiences = yield take(channel);
yield put(new ExperiencesFetchSucceeded(experiences));
}
} finally {
if (yield cancelled()) {
channel.close();
}
}
}
subscribeToExperiences uses a redux-saga eventChannel. An eventChannel receives an emmiter that generates a saga effect to be consumed with take. The eventChannel has to return a function to close the connections but afaik .onSnapshot connections don't need to be explicitly closed, that's why I return a dummy function.

Modifying array in redux store

I am working on a web app built on react + redux + thunk.
I have action creators as below. There are 2 async API calls, first fetchUser() will get user related info from the server, then fetchUserComments is another async call which returns me all the user comments.
export function fetchUserInfoFlow () {
return (dispatch, getState) => {
return dispatch(fetchUser()).then(() => {
return dispatch(fetchUserComments()
})
}
}
export function fetchUser() {
return (dispatch, getState) => {
return axios.get('urlhere')
.then(function (response) {
// basically fetch user data and update the store
dispatch({type: FETCH_USERS, user:response.data)
})
.catch(function (error) {
console.error(error)
})
}
}
export function fetchUserComment() {
return (dispatch, getState) => {
return axios.get('urlhere')
.then(function (response) {
// fetch user comments, get the user list from store, matching the user id and append the comments list to the user
let user = {...getState().user}
response.data.forEach(function (userComment) {
user.forEach(function (userComment) {
if(user.userId = userComment.userId){
user.comments = userComment.comments
}
}
}
dispatch({type: UPDATE_USER_COMMENT, user:response.data)
})
.catch(function (error) {
console.error(error)
})
}
}
I have several action creators similar to the above fetching user's related info and finally updating modifying the store's user list.
The problem is that the component does not re-render when the state updated, suspect if I have mutated the user list somewhere.
And I have read several articles that using getSate() in action creator should be only used in several cases and should not update its values as this will mutates the state.
In this cases, where should I put the logic of modifying the user list and how to avoid mutating the user list?
Thanks a lot!
This part should just go into your reducer, since thats the only place you should be making changes to the state:
let user = {...getState().user}
response.data.forEach(function (userComment) {
user.forEach(function (userComment) {
if(user.userId = userComment.userId){
user.comments = userComment.comments //mutating state
}
}
}

Resources