Vue-tables-2(vuex) reactivity not working - vue.js

I've several components using vue-tables-2 but one of them is not updating the table until I change the route.
component
<template>
//..
<div class="table-responsive" >
<v-client-table ref="table" name="vCardTable"
:data="vCardTableData.data"
:columns="vCardTableData.headers"
:options="vCardTableData.options"/>
</div>
//..
</template>
<script>
import { mapState } from "vuex";
import { mapGetters } from "vuex";
export default {
name: "VCard",
computed: {
...mapState("commons", ["user"]),
...mapGetters({ vCardTableData: "vCard/vCardTableData" })
},
mounted() {
var self = this;
self.$nextTick(() => {
self.$store.dispatch("vCard/getVCards"); <-- GET TABLE DATA
});
}
};
</script>
store
const state = {
vCardTableData: {
data: [],
headers: [
//..
],
options: {
filterable: false,
preserveState: false,
headings: {
//..
},
texts: {
//..
},
pagination: {
dropdown: true,
},
templates: {
//..
},
},
}
}
const getters = {
vCardTableData: state => state.vCardTableData
}
const actions = {
getVCards({commit, dispatch}) {
return api.request("get", "getvcards").then(response => {
setTimeout(() => {
commit("setVCardTableData", response.data.vcards);
}, 300);
}).catch(error => {
console.log(error);
});
}
}
const mutations = {
clearTableData: (state) => {
if (state.vCardTableData.data) {
state.vCardTableData.data = [];
}
},
setVCardTableData : (state, vCardTableData) => state.vCardTableData.data = vCardTableData
}
As you can see in this image the table has data:
But the view is refreshed when the route changes:
02/05/2018
Well now I've seen that if I modify the state directly in the component with promises it works:
this.$store.dispatch("vCard/getVCards", []).then((responseData)=>{
this.$store.state.vCard.vCardTableData.data = responseData;
});
Does anyone know why?
Thank you

My last answer was wrong, I did not remember that I had changed the vuex parameter of the table to false. I don't know why but doing a push it works:
setVCardTableData : (state, vCardTableData) => {
vCardTableData.forEach(tableData => {
state.vCardTableData.data.push(tableData);
});
}

This is a probably a reactivity issue. (See https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats for detailed explanation.)
Changing how you set the object value in your mutation to this should solve the problem
setVCardTableData: (state, vCardTableData) => state.vCardTableData = {
...state.vCardTableData,
data: vCardTableData
}
Basically, this creates a new object so that Vue knows that the object has been updated. In Javasript, object is passed by reference, meaning that vCardTableData don't store the object, it stores the reference to the object. You could think of it as a pointer/address that points to the object in memory. When you change a child property in the object, the reference remains unchanged, so Vue does not know that the object has been updated. Creating a new object makes sure that the object reference is updated.
This is also explained in Mutations Follow Vue's Reactivity Rules
in https://vuex.vuejs.org/en/mutations.html

I have had similar issues. Like others have already mention it is probably a reactivity problem. You can use Vue.set() to ensure that your properties are reactive when setting the state values.
setVCardTableData: (state, vCardTableData) => {
Vue.set(state.vCardTableData, 'data', vCardTableData);
}
Check the official documentation for this method.

Since it's a deep object, you need to use Object.assign in your mutation
setVCardTableData: (state, vCardTableData) => Object.assign(state.vCardTableData.data, vCardTableData)

Related

Unabled to use Vuex Getter with params

I have Vuex Store that will look like this
const config = {
featureA: { isEnabled: true, maxUser: 2 },
featureB: { isEnabled: false, maxData: 5 },
}
const actions = {
getDataCompany(context, payload) {
return new Promise(async (resolve, reject) => {
try {
const result = await firebase.firestore().collection(payload.collection).doc(payload.companyId).get()
if (result) {
if (payload.isLogin) await context.commit('setConfig', result.data())
return resolve(result.data())
}
reject(new Error('Fail To Load'))
} catch (e) {
reject(new Error('Connection Error'))
}
})
}
}
const mutations = {
setConfig(state, payload) {
state.config = payload
}
}
const getters = {
getData: ({ config }) => (feature, key) => {
const state = config
if (state) if (state[feature]) if (state[feature][key]) return state[feature][key]
return null
}
}
export default new Vuex.Store({
state: { config },
actions: { ...actions },
mutations: { ...mutations },
getters: { ...getters }
})
It's working fine with this method to get the data
computed: {
featureAEnabled() {
return this.$store.getters.getData('featureA', 'isEnabled')
},
}
But I have a problem when the data is change, the value is not update in component, and now I want to use mapGetters because it say can detect changes, But I have problem with the documentation and cannot find how to pass params here,
import { mapGetters } from 'vuex'
computed: {
...mapGetters({
featureAEnabled: 'getData'
})
}
I'am calling the action from here
async beforeMount() {
await this.$store.dispatch('getDataCompany', {collection: 'faturelsit', companyId: 'asep', isLogin: true})
}
And try to detect change in here
mounted() {
if (this.featureAEnabled) console.log('feature enabled')
}
The value change is not detected, and need to refresh twice before the changes is implemented in component
My main target is to detect if there any data change in Vuex and make action in component,
nevermind just working with watch without mapgetter,
I just realize that computed cannot re-run the mounted, so I make method that will called when the variable change in watch. thank you.
The main purpose is fulfilled, but the mapgetter with params is still not answered. so if anyone want to answer please share the way to use mapgetter with params.
You could try to use get and set methods for your computed property.
Example:
computed: {
featureAEnabled: {
get() {
return this.$store.getters.getData('featureA', 'isEnabled')
},
set(value) {
...update featureEnabled property in vuex store
}
},
}

How to get value of Vuex getter inside of Vue local data object

Hey this seemed to be pretty simple, but I just can't figure out how to get my loggedInUser object to work outside the template. I read through a bunch of other answers and I know I have to do something to store the getter in a local data object.
I have a temporary 'id' object set up because that's what I ultimately want to set to loggedInUser.id in order to attach to my Axios request.
Here's the page I want to make changes on:
<script>
import { mapGetters } from 'vuex'
// import vuex from 'vuex'
export default {
data: () => ({
results: "",
id: "15",
}),
computed: {
...mapGetters(['loggedInUser'])
},
// var id = {{loggedInUser}};
methods: {
getData() {
this.$axios.get('http://127.0.0.1:8000/api/v1/actors/',
{params: {user: this.id} }
)
.then(response => {this.results = response.data});
}
}
}
</script>
and here's my index.js for store:
export const getters = {
isAuthenticated(state) {
return state.auth.loggedIn
},
loggedInUser(state) {
return state.auth.user
}
}
The mapGetters helper simply maps store getters to local computed
properties
This:
computed: {
...mapGetters(['loggedInUser'])
},
Is equivalent to this:
computed: {
loggedInUser() {
return this.$store.getters.loggedInUser
},
},
Replace this.id with this.loggedInUser.id in your axios request, and get rid of the temporary id data property.
getData() {
this.$axios.get('http://127.0.0.1:8000/api/v1/actors/', {
params: {
user: this.loggedInUser.id
}
})
.then(response => {
this.results = response.data
});
}
We are assuming getData only gets called when the user is authenticated. If that's not the case, have in mind this.loggedInUser may be null or undefined and accessing this.loggedInUser.id will throw an error.

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.
}
}
}

VueJS - Accessing store data inside mounted

I'm having trouble understanding the following:
I have a store which contains variables needed for the application. In particular, there is a globalCompanies which stores:
globalCompanies: {
current: [],
all: [],
currentName: "",
}
Inside another component, I want to do the following:
mounted() {
this.$store.dispatch( "fetchUsers" );
var currentName = this.$store.state.globalCompanies.currentName;
console.log(currentName);
},
However, this just shows as empty. I know the value is there because I have computed which returns the currentName and it works fine inside the view itself. It just doesn't like the fact that it's in the mounted component.
Where am I going wrong and what can I do to resolve this issue? I really need to capture the companies Name in order to use it for some real time events.
As a result of our discussion:
In the question Vuex state value, accessed in component's mounted hook, returns empty value, because it is set in an async action which does not resolve before mounted executes.
When you need to trigger some function when async action in Vuex resolves with a value, you can achieve it using watch on a computed property, which returns a value from your Vuex state. When a value in store changes, the computed property reflects these changes and watch listener executes:
const store = new Vuex.Store({
state: {
globalCompanies: {
test: null
}
},
mutations: {
setMe: (state, payload) => {
state.globalCompanies.test = payload
}
},
actions: {
pretendFetch: ({commit}) => {
setTimeout(() => {
commit('setMe', 'My text is here!')
}, 300)
}
}
})
new Vue({
el: '#app',
store,
computed: {
cp: function() { // computed property will be updated when async call resolves
return this.$store.state.globalCompanies.test;
}
},
watch: { // watch changes here
cp: function(newValue, oldValue) {
// apply your logic here, e.g. invoke your listener function
console.log('was: ', oldValue, ' now: ', newValue)
}
},
mounted() {
this.$store.dispatch('pretendFetch');
// console.log(this.cp, this.$store.state.globalCompanies.test); // null
// var cn = this.$store.state.globalCompanies.test; // null
// console.log(cn) // null
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<script src="https://unpkg.com/vuex#2.3.1"></script>
<div id="app">
{{ cp }}
</div>
VueJS - Accessing Store Data Inside Mounted
Ran into this issue and it turned out to be a scope issue.
Store:
export default () => {
items:[],
globalCompanies:{
current:[],
all:[],
currentName: "Something"
},
ok: "Here you go"
}
Getters:
export default {
getGlobalCompanies(state){
return state.globalCompanies;
}
}
Mounted: This works...
mounted() {
// Initialize inside mounted to ensure store is within scope
const { getters } = this.$store;
const thisWorks = () => {
const globalCompanies = getters.getGlobalCompanies;
}
},
This is Bad: Reaching for the store outside the mounted scope
mounted() {
function ThisDontWork() {
const { getters } = this.$store; // this.$store == undefined
}
ThisDontWork();
},

Vuex rendering data that is fetched from REST API

For such component
<template>
<div>
<router-link :to="{name:'section', params: { sectionId: firstSectionId }}">Start</router-link>
</div>
</template>
<script lang="ts">
import { mapActions } from "vuex"
export default {
mounted() {
this.getSectionId()
},
computed: {
firstSectionId() {
return this.$store.state.firstSectionId
}
},
methods: mapActions(["getSectionId"])
}
</script>
Store:
const store: any = new Vuex.Store({
state: {
firstSectionId: null
},
// actions,
// mutations
})
I have a web request in the getSectionId action and it asynchronously fetches data and calls a mutation that will fill firstSectionId in state. During the initial rendering firstSectionId is null and I get the warning that a required parameter is missing during rendering of router-link.
It is not a problem here to add v-if="firstSectionId". But in general what is the approach for fetching data from a server to be displayed? Currently all my components are checking if there is data present in the store before rendering, is it normal or is there a better way to wait for data to be loaded before rendering it?
One approach for asynchronously fetching data is to use promise in vuex store actions.
Vue.http.get(API_URL)
.then((response) => {
//use response object
})
.catch((error) => {
console.log(error.statusText)
});
To demonstrate that I make request to this route. You can see how response should looks like. Let's save response object in state.users array.
store.js
const store = new Vuex.Store({
state: {
users: []
},
mutations: {
FETCH_USERS(state, users) {
state.users = users
}
},
actions: {
fetchUsers({ commit }, { self }) {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
self.filterUsers();
})
.catch((error) => {
console.log(error.statusText)
});
}
}
})
export default store
You noticed that there is self.filteruser() method after commit. That is crucial moment. Before that we are committing a mutation, which is synchronous operation and we are sure that we will have our response in store.state that can be used in filterUsers() method (don't forget to pass self parm)
Users.vue
import store from "../store/store"
export default {
name: 'users',
created() {
this.$store.dispatch("fetchUsers", { self: this })
},
methods:{
filterUsers() {
//do something with users
console.log("Users--->",this.$store.state.users)
}
}
}
Better ways (ES6 & ES7)
ES6 Promises for asynchronous programming
//User.vue
created() {
this.$store.dispatch("fetchUser").then(() => {
console.log("This would be printed after dispatch!!")
})
}
//store.js
actions: {
fetchUser({ commit }) {
return new Promise((resolve, reject) => {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
resolve();
})
.catch((error) => {
console.log(error.statusText);
});
});
}
}
ES7: async/await
To get away from callback hell, and to improve asynchronous programming use async function, and you can await on a promise. Code looks much easier to follow (like it is synchronous), but code isn't readable for browsers so you'll need Babel transpiler to run it.
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for actionA to finish
commit('gotOtherData', await getOtherData())
}
}
In my experience, you can skip a few checks if you preset the state with an empty value of the same type as the expected result (if you know what to expect, of course), e.g. if you have an array of items, start with [] instead of null as it won't break v-for directives, .length checks and similar data access attempts.
But generally, adding v-if is a very normal thing to do. There's a section about this in the vue-router documentation and checking whether properties exist or not is exactly what it suggests. Another possible solution it mentions is fetching data inside beforeRouteEnter guard, which assures you will always get to the component with your data already available.
Ultimately, both solutions are correct, and the decision between them is more of a UX/UI question.
I had similar requirements for locations and the google map api. I needed to fetch my locations from the API, load them in a list, and then use those in a map component to create the markers. I fetched the data in a Vuex action with axios, loaded that in my state with a mutation, and then used a getter to retrieve the resulting array in the mounted life cycle hook. This resulted in an empty array as mounted fired before the async action resolved.
I used store.subscribe to solve it this way:
<template>
<div class="google-map" :id="mapName"></div>
</template>
<script>
import GoogleMapsLoader from 'google-maps';
import { mapGetters } from 'vuex';
export default {
name: 'google-map',
props: ['name'],
computed: {
...mapGetters({
locations: 'locations/locations',
}),
},
data() {
return {
mapName: `${this.name}-map`,
};
},
mounted() {
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'locations/SAVE_LOCATIONS') {
GoogleMapsLoader.KEY = 'myKey';
GoogleMapsLoader.load((google) => {
/* eslint-disable no-new */
const map = new google.maps.Map(document.getElementById('locations-map'));
// loop through locations and add markers to map and set map boundaries
const bounds = new google.maps.LatLngBounds();
// I access the resulting locations array via state.module.property
state.locations.locations.forEach((location) => {
new google.maps.Marker({
position: {
lat: location.latitude,
lng: location.longitude,
},
map,
});
bounds.extend({
lat: location.latitude,
lng: location.longitude,
});
});
map.fitBounds(bounds);
});
}
});
},
};