How to overcome vuex "state property is read only" within a store mutation? - vuex

I have a vuex action that commits to a mutation:
updateStateProperty: (state, payload) => {
state.parent.child = payload.changer
}
This throws out a:
TypeError: ""child" is read-only"
I've seen this example solution, but it's missing explanation for deepClone purpose:
[types.SET_FIELD] (state, payload) {
let parent = deepClone(state.parent)
parent.child = payload.changer
state.parent = parent
}
Also seen other stuff but no clear code example.
How should I go about changing this property?

Related

Vuex store error when updating existing store item

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

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 Updated only in local scope of mutation

I am trying to update the store state using mutation. But the problem is that the state remains same after mutation and is updated only in scope of that mutation. It acting more of like passing arguments to a function.
The expectations are the state in the store should be updated but it's not happening. The store state is not mutated at all but the argument state is being mutated.
Here is the sample code of my implementation. I am using modules for Vuex Store and this is from
user.store.js
export const user = {
state: {
user: {},
isAuthenticated: false
},
mutations: {
updateUser(state, payload) {
Vue.set(state.user, payload.key, payload.value);
}
}
}
Edit 2:
I got to the fix of the issue and it is due to similar naming for the user store module and the state.user object.
After i changed the name of the store module, the issue was resolved.
Thank You for you contributions.
Try to use Vue.set in your mutation.
For example:
state: {
isInteractionEnabled: false
},
mutations: {
SWITCH_INTERACTION: (state, {status}) => Vue.set(state, 'isInteractionEnabled', status)
},

Better approach handling: 'Do not mutate vuex store state outside mutation handlers' errors

Beforehand: My application is working as intended, but I want to know if there's an better approach to the problem, I was having.
Situation: I have a project where I am currently implemeneting a Permission-System. The current flow is, to load specific objects (lets take user in this case) and inject the permissions afterwards.
Problem: Getting 'Do not mutate vuex store state outside mutation handlers.' error inside vuex-action.
Question: Is there a better way to omit the error than my approach below?
Simplified it looks like this (here I am getting my objects from our API and storing them in vuex-store):
// user.js (vuex-module)
state: {
user: null,
},
mutations: {
// ...
setUser(state, user) {
state.user = user
}
}
actions: {
// ... other vuex-actions
async login({commit, dispatch}, payload) {
let userFromDb = DbUtil.getUser(payload) // is an axios call to our api
// here the permissions get injected
// action in another vuex-module
dispatch('permissions/injectPermissions', userFromDb)
// commiting to store
commit('setUser', userFromDb)
return userFromDb
}
}
My permissions.js (here I am injecting the permissions to my object):
// permissions.js (vuex-module)
actions: {
// ... other vuex-actions
// payload = user in this example
async injectPermissions({commit, dispatch}, payload) {
let permissionFromDb = DbUtil.getPermissions(/* ... */)
payload.permissions = permissionFromDb // -> Here I am getting 'Do not mutate vuex store state outside mutation handlers.'-Error, because `payload` == the user from user-state
return payload
}
}
Workaround: I added a mutation which changes the user-state object for me inside a mutation-handler.
mutations: {
/**
* A 'set'-wrapper to mutate vuex-store variable inside a mutation to avoid getting a error.
* #param state
* #param payload:
* object - object to be mutated
* prop - prop inside object that is affected
* value - value that should be assigned
*/
setWrapper(state, payload) {
let { object, prop, value } = payload
object[prop] = value
}
}
The line where the error was thrown before gets changed to:
commit('setWrapper', {
object: payload,
prop: 'permissions',
value: permissionFromDb
})
Actions do not mutate the state.
Actions are there to perform asynchronous tasks.
When you want to change the state within an action, you have to rely on a mutation by using this syntax: commit('MUTATION_NAME', payload)
Then:
MUATATION_NAME(state, payload) {
state.permissions = payload.permissions
}
This is the cleanest and most correct way.