I believe that having an understanding of the motivations behind Mutations and Actions allows one to better judge when to use which and how. It also frees the programmer from the burden of uncertainty in situations where the "rules" become fuzzy. After reasoning a bit about their respective purposes, I came to the conclusion that although there may definitely be wrong ways to use Actions and Mutations, I don't think that there's a canonical approach.
Let's first try to understand why we even go through either Mutations or Actions.
Why go through the boilerplate in the first place? Why not change state directly in components?
Strictly speaking you could change the state
directly from your components. The state
is just a JavaScript object and there's nothing magical that will revert changes that you make to it.
// Yes, you can!
this.$store.state['products'].push(product)
However, by doing this you're scattering your state mutations all over the place. You lose the ability to simply just open a single module housing the state and at a glance see what kind of operations can be applied to it. Having centralized mutations solves this, albeit at the cost of some boilerplate.
// so we go from this
this.$store.state['products'].push(product)
// to this
this.$store.commit('addProduct', {product})
...
// and in store
addProduct(state, {product}){
state.products.push(product)
}
...
I think if you replace something short with boilerplate you'll want the boilerplate to also be small. I therefore presume that mutations are meant to be very thin wrappers around native operations on the state, with almost no business logic. In other words, mutations are meant to be mostly used like setters.
Now that you've centralized your mutations you have a better overview of your state changes and since your tooling (vue-devtools) is also aware of that location it makes debugging easier. It's also worth keeping in mind that many Vuex's plugins don't watch the state directly to track changes, they rather rely on mutations for that. "Out of bound" changes to the state are thus invisible to them.
So mutations
, actions
what's the difference anyway?
Actions, like mutations, also reside in the store's module and can receive the state
object. Which implies that they could also mutate it directly. So what's the point of having both? If we reason that mutations have to be kept small and simple, it implies that we need an alternative means to house more elaborate business logic. Actions are the means to do this. And since as we have established earlier, vue-devtools and plugins are aware of changes through Mutations, to stay consistent we should keep using Mutations from our actions. Furthermore, since actions are meant to be all encompassing and that the logic they encapsulate may be asynchronous, it makes sense that Actions would also simply made asynchronous from the start.
It's often emphasized that actions can be asynchronous, whereas mutations are typically not. You may decide to see the distinction as an indication that mutations should be used for anything synchronous (and actions for anything asynchronous); however, you'd run into some difficulties if for instance you needed to commit more than one mutations (synchronously), or if you needed to work with a Getter from your mutations, as mutation functions receive neither Getters nor Mutations as arguments...
...which leads to an interesting question.
Why don't Mutations receive Getters?
I haven't found a satisfactory answer to this question, yet. I have seen some explanation by the core team that I found moot at best. If I summarize their usage, Getters are meant to be computed (and often cached) extensions to the state. In other words, they're basically still the state, albeit that requires some upfront computation and they're normally read-only. That's at least how they're encouraged to be used.
Thus, preventing Mutations from directly accessing Getters means that one of three things is now necessary, if we need to access from the former some functionality offered by the latter: (1) either the state computations provided by the Getter is duplicated somewhere that is accessible to the Mutation (bad smell), or (2) the computed value (or the relevant Getter itself) is passed down as an explicit argument to the Mutation (funky), or (3) the Getter's logic itself is duplicated directly within the Mutation, without the added benefit of caching as provided by the Getter (stench).
The following is an example of (2), which in most scenarios that I have encountered seems the "least bad" option.
state:{
shoppingCart: {
products: []
}
},
getters:{
hasProduct(state){
return function(product) { ... }
}
}
actions: {
addProduct({state, getters, commit, dispatch}, {product}){
// all kinds of business logic goes here
// then pull out some computed state
const hasProduct = getters.hasProduct(product)
// and pass it to the mutation
commit('addProduct', {product, hasProduct})
}
}
mutations: {
addProduct(state, {product, hasProduct}){
if (hasProduct){
// mutate the state one way
} else {
// mutate the state another way
}
}
}
To me, the above seems not only a bit convoluted, but also somewhat "leaky", since some of the code present in the Action is clearly oozing from the Mutation's internal logic.
In my opinion, this is an indication of a compromise. I believe that allowing Mutations to automatically receive Getters presents some challenges. It can be either to the design of Vuex itself, or the tooling (vue-devtools et al), or to maintain some backward compatibility, or some combination of all the stated possibilities.
What I don't believe is that passing Getters to your Mutations yourself is necessarily a sign that you're doing something wrong. I see it as simply "patching" one of the framework's shortcomings.
mutations
andactions
are definted in the vuex documentation as methods of changing state. You don't need an action to commit a mutation. – Graham