Vuex store error when updating existing store item - vue.js

Little confused here...
I am trying to update a nested object in a Vuex state, which seems to work until I try to add another new value.
Here is my mutation
export const mutations = {
add(state, block) {
state.blocks.push({
type: block,
data: {},
style: {}
})
},
modify(state, [i, key, obj]) {
state.blocks[i][key] = Object.assign({}, state.blocks[i][key], obj)
},
remove(state, index) {
state.blocks.splice(index, 1)
console.log(index)
}
}
Actions:
export const actions = {
createBlock(context, type) {
context.commit('add', type);
},
updateBlock(context, payload) {
context.commit('modify', payload);
},
removeBlock(context, index) {
context.commit('remove', index)
}
}
And my dispatch of the action
this.$store.dispatch('updateBlock', [this.index, 'data', this.obj]) // works
this.$store.dispatch('updateBlock', [this.index, 'style', {m: newMargin}]) //throws error
When I update a Block with the type being data, things work, though when I try to add new data to the style object I get an error
[vuex] do not mutate vuex store state outside mutation handlers.
The end goal is to be able to add key/values to the styles object in the block. This will allow me to create dynamic class names.
What the heck am I missing? I feel like it has to do with Object.assign

Related

Nuxt store getter not working, ID given to payload is not an Integer + Error: [vuex] do not mutate vuex store state outside mutation handlers

I am trying to make a product detail page. The detail page is named _id.
When opened the id is replaced with the product id. On opening the page the state is set with data fetched from an api.
After that i am trying to use a computed property that refers to a getter named getProduct() with an id (this.$route.params.id) in the payload.
This is how my _id.vue looks like:
methods: {
...mapActions("products", ["fetchProducts",]),
...mapGetters("products", ["getProduct",]),
},
async mounted() {
this.fetchProducts()
},
computed: {
product() {
return this.getProduct(this.$route.params.id)
}
}
This is how my store file named products.js looks like:
import axios from "axios"
export const state = () => ({
producten: []
})
export const mutations = {
setProducts(state, data) {
state.producten = data
}
}
export const getters = {
getProduct(state, id) {
console.log(id)
return state.producten.filter(product => product.id = id)
}
}
export const actions = {
async fetchProducts({ commit }) {
await axios.get('/api/products')
.then(res => {
var data = res.data
commit('setProducts', data)
})
.catch(err => console.log(err));
}
}
What works is creating the state, but when i try to use the getter something goes wrong.
As you can see i console.log() the id given to it. Which logs the following:
I also get the error: client.js?06a0:103 Error: [vuex] do not mutate vuex store state outside mutation handlers.
Which I'm not doing as far as I know?
**Note: **these errors get logged as much as the length of my state array is.
From the Vuex documentation:
Vuex allows us to define "getters" in the store. You can think of them as computed properties for stores. Like computed properties, a getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.
Like computed, getters does not support having arguments.
But there is a way to have "method-style access" to a getter: https://vuex.vuejs.org/guide/getters.html#property-style-access
You can also pass arguments to getters by returning a function. This is particularly useful when you want to query an array in the store:
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
Note that getters accessed via methods will run each time you call them, and the result is not cached.

Updating getter value Vuex store when state changes

I'm trying to figure out how to properly update a getter value when some other variable from VueX changes/updates.
Currently I'm using this way in a component to update:
watch: {
dates () {
this.$set(this.linedata[0].chartOptions.xAxis,"categories",this.dates)
}
}
So my getter linedata should be updated with dates value whenever dates changes. dates is state variable from VueX store.
The thing is with this method the value won't be properly updated when I changed route/go to different components. So I think it's better to do this kind of thing using the VueX store.
dates is updated with an API call, so I use an action to update it.
So the question is how can I do such an update from the VueX store?
EDIT:
I tried moving this to VueX:
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit("SET_DATA", {
this.linedata[0].chartOptions.xAxis,"categories": response.data.dates1,
this.linedata[1].chartOptions.xAxis,"categories": response.data.dates2
});
}
SET_DATA(state, payload) {
state = Object.assign(state, payload);
}
But the above does not work, as I cannot set nested object in action this way...
Getters are generally for getting, not setting. They are like computed for Vuex, which return calculated data. They update automatically when reactive contents change. So it's probably best to rethink the design so that only state needs to be updated. Either way, Vuex should be updated only with actions/mutations
Given your example and the info from all your comments, using linedata as state, your action and mutation would look something like this:
actions: {
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit('SET_DATA', response.data.dates);
}
}
mutations: {
SET_DATA(state, dates) {
Vue.set(state.linedata[0].chartOptions.xAxis, 'categories', dates[0]);
Vue.set(state.linedata[1].chartOptions.xAxis, 'categories', dates[1]);
}
}
Which you could call, in the component for example, like:
this.$store.dispatch('loadData');
Using Vue.set is necessary for change detection in this case and requires the following import:
import Vue from 'vue';
Theoretically, there should be a better way to design your backend API so that you can just set state.linedata = payload in the mutation, but this will work with what you have.
Here is a simple example of a Vuex store for an user.
export const state = () => ({
user: {}
})
export const mutations = {
set(state, user) {
state.user = user
},
unset(state) {
state.user = {}
},
patch(state, user) {
state.user = Object.assign({}, state.user, user)
}
}
export const actions = {
async set({ commit }) {
// TODO: Get user...
commit('set', user)
},
unset({ commit }) {
commit('unset')
},
patch({ commit }, user) {
commit('patch', user)
}
}
export const getters = {
get(state) {
return state.user
}
}
If you want to set the user data, you can call await this.$store.dispatch('user/set') in any Vue instance. For patching the data you could call this.$store.dispatch('user/patch', newUserData).
The getter is then reactively updated in any Vue instance where it is mapped. You should use the function mapGetters from Vuex in the computed properties. Here is an example.
...
computed: {
...mapGetters({
user: 'user/get'
})
}
...
The three dots ... before the function call is destructuring assignment, which will map all the properties that will the function return in an object to computed properties. Those will then be reactively updated whenever you call dispatch on the user store.
Take a look at Vuex documentation for a more in depth explanation.

Use getter in the same module in which it was created

Is it possible to initialize state's property using getter which was created in the same module? Something like this:
export const gamesModule = {
state: {
games: [],
selectedGameID: null,
playerOnTurnID: this.getters.getSelectedGame.playerData[0]
},
getters: {
getGames: state => state.games,
getselectedGameID: state => state.selectedGameID,
getSelectedGame: state => getSelectedGameById(state.games, state.selectedGameID),
},
mutations: {
SET_GAMES (state, game) {
state.games.push(game);
},
SET_SELECTED_GAME_ID (state, id) {
state.selectedGameID = id;
},
SET_PLAYER_ON_TURN_ID (state, playerID) {
state.playerOnTurnID = playerID;
}
},
actions: {
async createGame({ commit }) {
try {
const { data } = await gameService.createGame();
commit('SET_GAMES', data);
} catch (error) {
console.warn('Error creating new game: ', error);
}
},
setSelectedGameID({ commit }, id) {
commit('SET_SELECTED_GAME_ID', id);
},
};
Written like this, it does not work because getters are undefined.
this does not exist in an object's context, and is only really applicable in constructor functions or classes.
I see two problems here.
First of all, you can't reference the object itself, because it hasn't been defined yet. You would have to create a local variable before declaring the object that would have the common property, in this case, the getter function.
Second of all, more importantly, I'm not sure it would help to access the getter (Reducer) function, as it has no knowledge of the state, which is passed to it as the first parameter by the underlying Vuex library when processing mutations (Actions).
Vuex is based upon the Redux pattern, Action -> Reducer -> Store, I would recommend reading it a quick introduction on how Redux works, as it will help you understand a lot better the action flow inside of Vuex.

Vuex state change on object does not trigger rerender

I have a variable in the vuex store called permissions. And i want my component to trigger a rerender when the getPermissions changes. In the vue devtools i clearly see that the state has changed in the store, but the component stil get the old state from getPermissions. In order for me to see changes, I have to do a refresh. Has it something to do with the way i mutate it? or the fact that it is an object?
It looks like this when populated:
permissions: {
KS1KD933KD: true,
KD9L22F732: false
}
I use this method to do mutations on it and a getter to get it:
const getters = {
getPermissions: state => state.permissions
};
const mutations = {
set_recording_permissions(state, data) {
let newList = state.permissions;
newList[data.key] = data.bool;
Vue.set(state, 'permissions', newList);
}
};
And in the component i use mapGetters to get access to it
computed: {
...mapGetters('agentInfo',['getPermissions'])
}
In order to update the permissions value i use this action (it does require a succesfull api request before updating the value) :
const actions = {
async setRecordingPermissions({ commit }, data) {
let body = {
agentId: data.userName,
callId: data.callId,
allowUseOfRecording: data.allowUseOfRecording
};
try {
await AgentInfoAPI.editRecordingPermissions(body).then(() => {
commit('set_recording_permissions', { key: data.callId, bool: data.allowUseOfRecording });
commit('set_agent_info_message', {
type: 'success',
text: `Endret opptaksrettigheter`
});
});
} catch (error) {
console.log(error);
commit('set_agent_info_message', {
type: 'error',
text: `Request to ${error.response.data.path} failed with ${error.response.status} ${error.response.data.message}`
});
}
}
}
Since the getter only returns state variable you should use mapState, if you want to access it directly.
computed: mapState(['permissions'])
However, you can also use mapGetters, but then in your template, have to use getPermissions and not permissions.
Example template:
<ul id="permissions">
<li v-for="permission in getPermissions">
{{ permission }}
</li>
</ul>
If you have done this it is probably an issue with the object reference. You use Vue.set, but you set the same object reference. You have to create a new object or set the key you want to update directly.
new object
let newList = { ...state.permissions };
Vue.set
Vue.set(state.permission, data.key, data.value);
I don't know what the rest of you code looks like, but you will need to use actions to correctly mutate you store.
For example:
const actions = {
setName({ commit }, name) {
commit('setName', name);
},
}

Unwanted Vuex store state mutation triggered

I have a mixin for my view components that I use to set the metadata for each page. I'm using Vuex to fetch the default metadata in my store. When including the mixin in each component, I add the "metadata" data attribute and I set the value to the defaultmetadata value in my store. However, when I reassign the value of "metadata" in my mixin / component, Vuex complains that I'm trying to do mutations outside of the store. But I'm just changing the value of "metadata" in my mixin. I'm not touching "defaultmetadata" in the store.
Any ideas ?
the mixin :
import store from 'Store/store'
export default {
data: function () {
return {
metadata: store.state.defaultmetadata
}
},
computed: {
getMetaData: function () {
return this.metadata
}
},
metaInfo () {
let res = {
title: this.getMetaData.title,
meta: [
{ name: 'description', content: this.getMetaData.description }
]
}
return res
},
methods: {
updateMetaData: function (key, value) {
let regex = /(<([^>]+)>)/ig
if (this.textNotEmpty(value)) {
this.$set(this.metadata, key, value.replace(regex, ''))
return true
} else {
return false
}
}
},
beforeRouteEnter (to, from, next) {
next(vm => {
})
}
}
This line that causes the mutation on the store :
this.$set(this.metadata, key, value.replace(regex, ''))
Here, I'm setting the local data attribute "metadata". If I comment the line, Vuex stops complaining. Why does this line trigger a mutation of store data ?
[Vue warn]: Error in callback for watcher "function () { return this._data.$$state }": "Error: [vuex] do not mutate vuex store state outside mutation handlers."
They still point to a same reference, which is known as shallow copy. You can use JSON.parse(JSON.stringify)) to create a deep copy
data: function () {
return {
metadata: JSON.parse(JSON.stringify(store.state.defaultmetadata))
}
},