Using store data in Component pulls in default values, instead of updated ones - vue.js

I am new to Vue, and am trying to use data from a store in a component. This data gets set when the app loads, and I have confirmed that it is getting set properly.
However, when I try to access this data from the store in a component (I'm using mapState), all I get are the default values for the store, and not the data that was set.
The store I am trying to access is:
//user.js
const defaults = {
emailAddress: '',
gender: 'N/A',
registered: false,
userName: '',
};
const state = {
user: { ...defaults, },
};
const getters = {};
const actions = {
login ({ commit, }) {
api.post(/loginurl).then((response) => {
commit('setUserInfo', { ...defaults, ...response, });
});
},
};
const mutations = {
setUserInfo (state, user) {
state.user = {...state, ...user,};
},
};
const user = {
namespaced: true,
state,
getters,
actions,
mutations,
};
export default user;
I've confirmed that all data is getting set correctly in setUserInfo.
However, when I access this in a component like so:
<template>
<div class="user-info">
<p> {{ user }} </p>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState('user', { user: (state) => state.user, },),
}
};
</script>
All the defaults for user print out in my template, instead of the values that were set in 'setUserInfo.
I will also add, that if I just try to access this via a computed property directly, I also get the defaults. For example:
userInfo () {
return this.$store.state.user;
},
Will also just return the defaults.
Do I need to set up a getter in the store, and then just use that in my component instead? Any help would be much appreciated!!!

Related

Problems with vuex states

When my application is started I make two requests to get some information (company and user informations) that I will use basically in all my components. I store this information in store.js
My App.vue
<template>
<router-view></router-view>
</template>
export default{
methods: {
getCompanyIndormation(){
//Vuex function to store company information
this.setCompanyInformation(someinformation)
},
getUserInformation(){
//Vuex function to store user information
this.setUserInformation(someinformation)
}
}
}
My store.js
export default new Vuex.Store({
state: {
user: {
id: '',
name: '',
},
company: {
name: '',
subdomain: ''
}
},
mutations: {
setUserInformation: (state, obj) => {
state.user.id = obj.id;
state.user.name = obj.name;
},
setCompanyInformation: (state, obj) => {
state.company.name = obj.name;
state.company.subdomain = obj.subdomain;
}
}
})
So far everything works perfectly. My problem was when I tried to retrieve some information from the company in a mixin I own.
My mixin.js
import { mapState } from 'vuex';
const myMixin = {
computed: {
...mapState(['company'])
},
methods: {
$getCompanyUrl(){
return 'https:// '+this.company.subdomain+'/contact'
}
}
}
My problem here is that in some cases the function of my mixin normally returns the entire url, for example: https://domain1.com/contact but sometimes it returns https:///contact, that is, he didn't find it still the domain. Does anyone have any suggestions on how I can solve this problem? Do I add a watcher to see when the company's information has changed in the store or do I expect to finish all initial requirements before even rendering the router-view?
Maybe just create a getter for company url in your store.
getters: {
companyUrl: state => `https://${state.company.subdomain}/contact`,
},
And then use mapGetters anywhere else. The getters are meant to be watching the state changes.

Clone / Copy state is returning empty state

I am having an issue using lodash's cloneDeep to clone the user object passed in from the store. When I attempt to render the data in the template {{ user }} shows the data retrieved from the store and {{ userCopy }} shows the empty store. I am not sure why this is happening, I am new to Vue.
store/staff.js
import StaffService from '~/services/StaffService.js'
export const state = () => ({
user: {
offers: '',
legal: ''
}
})
export const mutations = {
SET_USER(state, user) {
state.user = user
},
}
export const actions = {
fetchUser({ commit, getters }, id) {
const user = getters.getUserById(id)
if (user) {
commit('SET_USER', user)
} else {
StaffService.getUser(id)
.then((response) => {
commit('SET_USER', response.data)
})
.catch((error) => {
console.log('There was an error:', error.response)
})
}
},
}
export const getters = {
getUserById: (state) => (id) => {
return state.staff.find((user) => user.id === id)
}
}
pages/settings/_id.vue
<template>
<div>
{{ user }} // will display the whole object
{{ userCopy }} // will only display empty store object
</div>
</template>
<script>
import _ from 'lodash'
data() {
return {
userCopy: _.cloneDeep(this.$store.state.staff.user)
}
},
computed: {
...mapState({ user: (state) => state.staff.user })
},
created() {
this.$store.dispatch('staff/fetchUser', this.$route.params.id)
},
</script>
My guess would be that a Vue instance's data is initialized before state becomes available. While computed props are populated/updated as their data source change.
If the component doesn't need to change the value of user during runtime, I'd suggest turning it into a computed property.
If your component does change the value during runtime (such as when it's v-model'd to an input), there are two approaches you can do.
Method 1: Using mounted hook
This is done by placing user in data property and then assigning a value when the instance is mounted, like so:
mounted () {
this.$data.userCopy = _.cloneDeep(this.$store.state.staff.user)
}
Method 2: Using computed with getter and setter functions.
Normally, you shouldn't change a computed value. But it can be done using a setter function. With this, when Vue detects an attempt to change a computed prop it will execute set() with the old and new values as arguments. This function would change the value at its source, allowing get()'s returned value to reflect this. For example:
computed: {
userCopy: {
get () {
return _.cloneDeep(this.$store.state.staff.user)
},
set (newValue) {
this.$store.commit('updateStaff', newValue) // Replace this line with your equivalent state mutator.
}
}
}

Setting value to input field using Vuex store modules

I have a vuex in module mode that fetching the data of a user:
store/modules/users.js
import axios from "axios";
export const state = () => ({
user: {}
});
// Sets the values of data in states
export const mutations = {
SET_USER(state, user) {
state.user = user;
}
};
export const actions = {
fetchUser({ commit }, id) {
console.log(`Fetching User with ID: ${id}`);
return axios.get(`${process.env.BASE_URL}/users/${id}`)
.then(response => {
commit("SET_USER", response.data.data.result);
})
.catch(err => {
console.log(err);
});
}
};
// retrieves the data from the state
export const getters = {
getUser(state) {
return state.user;
}
};
then on my template pages/users/_id/index.vue
<b-form-input v-model="name" type="text"></b-form-input>
export default {
data() {
return {
name: ""
}
},
created() {
// fetch user from API
this.$store.dispatch("fetchUser", this.$route.params.id);
}
}
Now I check the getters I have object getUser and I can see the attribute. How can I assign the name value from vuex getters to the input field?
watcher is probably what you need
export default {
// ...
watch: {
'$store.getters.getUser'(user) {
this.name = user.name;
},
},
}
While Jacob's answer isn't necessarily incorrect, it's better practice to use a computed property instead. You can read about that here
computed: {
user(){
return this.$store.getters.getUser
}
}
Then access name via {{user.name}} or create a name computed property
computed: {
name(){
return this.$store.getters.getUser.name
}
}
Edit: fiddle as example https://jsfiddle.net/uy47cdnw/
Edit2: Please not that if you want to mutate object via that input field, you should use the link Jacob provided.

currentUser not getting set with Vuex

I added some code to my vue project so I can save the state of a user - which is whether he is logged in or not. If the state is not null, I want to display the navbar and footer. I added all the vuex import statements. I am using an axios call to the db which returns a json response. response.data comes back as true/false. If true, I redirect the user to the main page. Then I create a user object called currentUser, but I'm not sure what to base it on, so it is getting set to null. I need to use the state in a few places throughout my app, but it is not getting set. Please someone help. Thanks in advance. (code is below)
User.js:
import JwtDecode from 'jwt-decode'
export default class User {
static from (token) {
try {
let obj = JwtDecode(token)
return new User(obj)
} catch (_) {
return null
}
}
constructor ({username}) {
this.username = username
}
}
App.vue:
<template>
<div id="app">
<template v-if="currentUser">
<Navbar></Navbar>
</template>
<div class="container-fluid">
<router-view></router-view>
<template v-if="currentUser">
<Foot></Foot>
</template>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Navbar from '#/components/Navbar'
import Foot from '#/components/Foot'
export default {
name: 'App',
components: {
Navbar,
Foot
},
computed: {
...mapGetters({ currentUser: 'currentUser' })
},
mutation_types.js:
export const LOGIN = 'LOGIN'
export const LOGOUT = 'LOGOUT'
auth.js:
/* global localStorage */
import User from '#/models/User'
import * as MutationTypes from './mutation_types'
const state = {
user: User.from(localStorage.token)
}
const mutations = {
[MutationTypes.LOGIN] (state) {
state.user = User.from(localStorage.token)
},
[MutationTypes.LOGOUT] (state) {
state.user = null
}
}
const getters = {
currentUser (state) {
return state.user
}
}
const actions = {
login ({ commit }) {
commit(MutationTypes.LOGIN)
},
logout ({ commit }) {
commit(MutationTypes.LOGOUT)
}
}
export default {
state,
mutations,
getters,
actions
}
The user store should only set the default state. AFter making request to validate user. You should use actions and mutations to set the user state. Call the action via store.dispatch("user/login", user) where you return new User(obj).
let obj = JwtDecode(token)
const user = new User(obj)
store.dispatch("user/login", user)
const actions = {
login ({ commit }, userObject) {
commit(MutationTypes.LOGIN, userObject)
},
logout ({ commit }) {
commit(MutationTypes.LOGOUT)
}
}
const mutations = {
[MutationTypes.LOGIN] (state, user) {
state.user = user;
},
[MutationTypes.LOGOUT] (state) {
state.user = null
}
}
On a other note, you have dumb getters. Meaning they just return the state. You can rather call the user object directly out of state. Use getters when you want to modify the return value before returning it.
I took a little look and it seems you use '=' instead of Vue.set() to set your state variable.
Refer to the answer : vuex - is it possible to directly change state, even if not recommended?

Making Async Calls With Vuex

I'm just starting to learn Vuex here. Until now I've been storing shared data in a store.js file and importing store in every module but this is getting annoying and I'm worried about mutating state.
What I'm struggling with is how to import data from firebase using Vuex. From what I understand only actions can make async calls but only mutations can update the state?
Right now I'm making calls to firebase from my mutations object and it seems to be working fine. Honestly, all the context, commit, dispatch, etc. seems a bit overload. I'd like to just be able to use the minimal amount of Vuex necessary to be productive.
In the docs it looks like I can write some code that updates the state in the mutations object like below, import it into my component in the computed property and then just trigger a state update using store.commit('increment'). This seems like the minimum amount necessary to use Vuex but then where do actions come in? Confused :( Any help on the best way to do this or best practices would be appreciated!
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
My code is below
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const db = firebase.database();
const auth = firebase.auth();
const store = new Vuex.Store({
state: {
userInfo: {},
users: {},
resources: [],
postKey: ''
},
mutations: {
// Get data from a firebase path & put in state object
getResources: function (state) {
var resourcesRef = db.ref('resources');
resourcesRef.on('value', snapshot => {
state.resources.push(snapshot.val());
})
},
getUsers: function (state) {
var usersRef = db.ref('users');
usersRef.on('value', snapshot => {
state.users = snapshot.val();
})
},
toggleSignIn: function (state) {
if (!auth.currentUser) {
console.log("Signing in...");
var provider = new firebase.auth.GoogleAuthProvider();
auth.signInWithPopup(provider).then( result => {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// Set a user
var uid = user.uid;
db.ref('users/' + user.uid).set({
name: user.displayName,
email: user.email,
profilePicture : user.photoURL,
});
state.userInfo = user;
// ...
}).catch( error => {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
} else {
console.log('Signing out...');
auth.signOut();
}
}
}
})
export default store
main.js
import Vue from 'vue'
import App from './App'
import store from './store'
new Vue({
el: '#app',
store, // Inject store into all child components
template: '<App/>',
components: { App }
})
App.vue
<template>
<div id="app">
<button v-on:click="toggleSignIn">Click me</button>
</div>
</template>
<script>
import Hello from './components/Hello'
export default {
name: 'app',
components: {
Hello
},
created: function () {
this.$store.commit('getResources'); // Trigger state change
this.$store.commit('getUsers'); // Trigger state change
},
computed: {
state () {
return this.$store.state // Get Vuex state into my component
}
},
methods: {
toggleSignIn () {
this.$store.commit('toggleSignIn'); // Trigger state change
}
}
}
</script>
<style>
</style>
All AJAX should be going into actions instead of mutations. So the process would start by calling your action
...which commits data from the ajax callback to a mutation
...which is responsible for updating the vuex state.
Reference: http://vuex.vuejs.org/en/actions.html
Here is an example:
// vuex store
state: {
savedData: null
},
mutations: {
updateSavedData (state, data) {
state.savedData = data
}
},
actions: {
fetchData ({ commit }) {
this.$http({
url: 'some-endpoint',
method: 'GET'
}).then(function (response) {
commit('updateSavedData', response.data)
}, function () {
console.log('error')
})
}
}
Then to call your ajax, you will have to call the action now by doing this:
store.dispatch('fetchData')
In your case, just replace this.$http({...}).then(...) with your firebase ajax and call your action in the callback.