Can you commit a mutation from a getter call? - vuex

My Vuex module looks something like the below snippet. I use a getter to get various values from the parent object based on some logic in the parameters, (e.g. removing some options from a dropdown box based on other values in the object), and for example if something in the state would cause the returned value to have the locked property set to true, I want to fire off a mutation call to update the main object. I could do this on the vue level (where I get the computed values) using a dispatch call, but I'd rather not have to maintain that dispatch call in every place my getter is used. Can I call the mutation from the getter call directly? I've tried doing it as shown below but it keeps telling me commit is not a function.
var module = {
state: {
myObj: {
propertyA: { /* object with multiple different children */ },
}
},
getters: {
getField: state => params => {
switch(params.option) {
/*logic based on current state values*/
var ret = state.PropertyA[params.Field];
if(ret.conditionalProperty) {
// update state.Property[otherKey]
commit('updateField', { myValue: true }); // The problem in question
}
return ret;
}
},
mutations: {
updateField(state, payload) { state.PropertyA.Field = payload },
}
}

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

Vuex: Add Dynamic Property to Object without triggering Watchers on existing Properties

I have a Vuex store with an object:
state: {
contents: {},
}
where I dynamically store contents by key:
mutations: {
updateContent: (state, { id, a, b }) => {
Vue.set(state.contents, id, { a, b });
},
}
and get them using:
getters: {
content: (state) => (id) => {
if (id in state.contents) return state.contents[id];
return [];
}
},
Let's say I have a component like this:
export default {
props: ["id"],
computed: {
myContent() {
return this.$store.getters.content(this.id)
}
},
// ...
}
How can I add dynamic properties using the mutation without triggering changes in components watching unchanged, already existant properties of state.contents?
Also see this fiddle.
If you want to watch inner property of objects, you can use deep watchers.
In your situation, i am assuming you're setting properly yor getters, setter and update methods. You should add this to your watchers:
// ...
watch:{
id: {
deep: true,
handler(newVal, oldVal){
console.log("New value and old value: ", newVal, oldVal")
// ... make your own logic here
}
}
}
Let me explain little bit more above code, when we want to watch inner property of any object in Vue, we should use deep and handler function to manipulate in every change. (remember that, handler is not a random name, it's reserved keyword)
I'm trying to figure it out by checking the fiddle: https://jsfiddle.net/ericskaliks/Ln0uws9m/15/ , and I have a possible reason to this behavior.
Getters and Computed properties are updated or reached when the observed objects or properties inside them are changed. In this case the content getter is "watching" the state.contents store property, so each time store.contents is updated, the content getter is called, then the computed property myContent() updates its value and increase the updated data property with this.update++.
So the components will always be updated if the state.contents property is updated, i.e. adding a new unrelated property.
One could use a global event bus instead of a vuex getter:
const eventBus = new Vue();
components can subscribe to the element they need:
watch: {
id: {
handler(n, o) {
if (o) eventBus.$off(o + "", this.onChange);
eventBus.$on(n + "", this.onChange);
this.$store.dispatch("request", { id: n });
},
immediate: true
}
},
and changes of which the components have to be notified are dispatched using an action:
actions: {
request(_, { id }) {
eventBus.$emit(id + "", this.getters.content(id));
},
updateContent({ commit }, { id, a, b }) {
commit("updateContent", { id, a, b });
eventBus.$emit(id + "", { a, b });
}
}
That way one can precisely control when which updates are fired.
fiddle.
It seems like Vue(x) can't do this out-of-the-box. But by adding an empty Observer (yes, this is a hack) you can make Vue-components temporarily nonreactive (based on this blogpost).
Basically - we have an Observer on the object itself and an Observer on each of the properties. We will destroy the Observer on the object. When doing so, we have to make sure, that when the getter is first called by a component it returns a reactive value rather then {}, because the getter can't observe adding a new property to the object anymore. Therefore, we add a touch-mutation initializing the object. This function needs the original Observer of the object to create Observers on its properties, causing one, but only one, unnecessary update of the component:
mutations: {
updateContent: (state, { id, a, b }) => {
Vue.set(state.contents, id, { a, b });
},
touch: (state, { id }) => {
if(id in state.contents) return
if (myObserver === null)
myObserver = state.contents.__ob__
state.contents.__ob__ = myObserver
Vue.set(state.contents, id, {});
state.contents.__ob__ = new Observer({});
}
},
The constructor can be obtained using:
const Observer = (new Vue()).$data.__ob__.constructor;
Our component has to call touch whenever the id changes:
props: ["id"],
watch: {
i: {
immediate: true,
handler() {
this.$store.commit("touch", { id: this.id })
}
}
},
computed: {
myContent() {
return this.$store.getters.content(this.id)
}
},
As you can see in this fiddle, adding new properties to the object doesn't trigger unnecessary updates anymore.

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.

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.

Make Vue template wait for global object returned by AJAX call

I'm trying to wait for certain strings in a sort of dictionary containing all the text for buttons, sections, labels etc.
I start out by sending a list of default strings to a controller that registers all the strings with my CMS in case those specific values do not already exist. After that I return a new object containing my "dictionaries", but with the correct values for the current language.
I run the call with an event listener that triggers a dispatch() on window.onload, and then add the data to a Vuex module state. I then add it to a computed prop.
computed: {
cartDictionary() {
return this.$store.state.dictionaries.myDictionaries['cart']
}
}
So now here's the problem: In my template i try to get the values from the cartDictionaryprop, which is an array.
<h2 class="checkout-section__header" v-html="cartDictionary['Cart.Heading']"></h2>
But when the component renders, the prop doesn't yet have a value since it's waiting for the AJAX call to finish. And so of course I get a cannot read property of undefined error.
Any ideas on how to work around this? I would like to have the dictionaries accessible through a global object instead of passing everything down through props since it's built using atomic design and it would be insanely tedious.
EDIT:
Adding more code for clarification.
My module:
const dictionaryModule = {
namespaced: true,
state: {
dictionaries: []
},
mutations: {
setDictionaries (state, payload) {
state.dictionaries = payload
}
},
actions: {
getDictionaries ({commit}) {
return new Promise((resolve, reject) => {
Dictionaries.init().then(response => {
commit('setDictionaries', response)
resolve(response)
})
})
}
}
}
My Store:
const store = new Vuex.Store({
modules: {
cart: cartModule,
search: searchModule,
checkout: checkoutModule,
filter: filterModule,
product: productModule,
dictionaries: dictionaryModule
}
})
window.addEventListener('load', function () {
store.dispatch('dictionaries/getDictionaries')
})
I think you can watch cartDictionary and set another data variable.
like this
<h2 class="checkout-section__header" v-html="cartHeading"></h2>
data () {
return {
cartHeading: ''
}
},
watch: {
'cartDictionary': function (after, before) {
if (after) {
this.cartHeading = after
}
}
}
Because this.$store.state.dictionaries.myDictionarie is undefined at the the begining, vuejs can't map myDictionarie['core']. That's why your code is not working.
You can do this also
state: {
dictionaries: {
myDictionaries: {}
}
}
and set the dictionaries key values during resolve.
I also would have liked to see some more of your code, but as i can't comment your questions (you need rep > 50), here it goes...
I have two general suggestions:
Did you setup your action correctly? Mutations are always synchronous while actions allow for asynchronous operations. So, if you http client returns a promise (axios does, for example), you should await the result in your action before calling the respective mutation. See this chapter in the official vuex-docs: https://vuex.vuejs.org/guide/actions.html
You shouldn't be using something like window.onload but use the hooks provided by Vue.js instead. Check this: https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
EDIT: As a third suggestion: Check, whether action and mutation are called properly. If they are handled in their own module, you have to register the module to the state.