In Vuex, what is the logic of having both \"actions\" and \"mutations?\"
I understand the logic of components not being able to modify state (which seems smart), but
Disclaimer - I've only just started using vuejs so this is just me extrapolating the design intent.
Time machine debugging uses snapshots of the state, and shows a timeline of actions and mutations. In theory we could have had just actions
alongside a recording of state setters and getters to synchronously describe mutation. But then:
mutations
transactions but then we can say the transaction needs to be improved as opposed to it being a race condition in the actions. Anonymous mutations inside an action could more easily resurface these kinds of bugs because async programming is fragile and difficult.Compare the following transaction log with named mutations.
Action: FetchNewsStories
Mutation: SetFetchingNewsStories
Action: FetchNewsStories [continuation]
Mutation: DoneFetchingNewsStories([...])
With a transaction log that has no named mutations:
Action: FetchNewsStories
Mutation: state.isFetching = true;
Action: FetchNewsStories [continuation]
Mutation: state.isFetching = false;
Mutation: state.listOfStories = [...]
I hope you can extrapolate from that example the potential added complexity in async and anonymous mutation inside actions.
https://vuex.vuejs.org/en/mutations.html
Now imagine we are debugging the app and looking at the devtool's mutation logs. For every mutation logged, the devtool will need to capture a "before" and "after" snapshots of the state. However, the asynchronous callback inside the example mutation above makes that impossible: the callback is not called yet when the mutation is committed, and there's no way for the devtool to know when the callback will actually be called - any state mutation performed in the callback is essentially un-trackable!
I think the TLDR answer is that Mutations are meant to be synchronous/transactional. So if you need to run an Ajax call, or do any other asynchronous code, you need to do that in an Action, and then commit a mutation after, to set the new state.
It might seem unnecessary to have an extra layer of actions
just to call the mutations
, for example:
const actions = {
logout: ({ commit }) => {
commit("setToken", null);
}
};
const mutations = {
setToken: (state, token) => {
state.token = token;
}
};
So if calling actions
calls logout
, why not call the mutation itself?
The entire idea of an action is to call multiple mutations from inside one action or make an Ajax request or any kind of asynchronous logic you can imagine.
We might eventually have actions that make multiple network requests and eventually call many different mutations.
So we try to stuff as much complexity from our Vuex.Store()
as possible in our actions
and this leaves our mutations
, state
and getters
cleaner and straightforward and falls in line with the kind of modularity that makes libraries like Vue and React popular.
1.From docs:
Actions are similar to mutations, the differences being that:
- Instead of mutating the state, actions commit mutations.
- Actions can contain arbitrary asynchronous operations.
The Actions can contain asynchronous operations, but the mutation can not.
2.We invoke the mutation, we can change the state directly. and we also can in the action to change states by like this:
actions: {
increment (store) {
// do whatever ... then change the state
store.dispatch('MUTATION_NAME')
}
}
the Actions is designed for handle more other things, we can do many things in there(we can use asynchronous operations) then change state by dispatch mutation there.
I have been using Vuex professionally for about 3 years, and here is what I think I have figured out about the essential differences between actions and mutations, how you can benefit from using them well together, and how you can make your life harder if you don't use it well.
The main goal of Vuex is to offer a new pattern to control the behaviour of your application: Reactivity. The idea is to offload the orchestration of the state of your application to a specialized object: a store. It conveniently supplies methods to connect your components directly to your store data to be used at their own convenience. This allows your components to focus on their job: defining a template, style, and basic component behaviour to present to your user. Meanwhile, the store handles the heavy data load.
That is not just the only advantage of this pattern though. The fact that stores are a single source of data for the entirety of your application offers a great potential of re-usability of this data across many components. This isn't the first pattern that attempts to address this issue of cross-component communication, but where it shines is that it forces you to implement a very safe behaviour to your application by basically forbidding your components to modify the state of this shared data, and force it instead to use "public endpoints" to ask for change.
The basic idea is this:
That being said, the magic begins when we start designing our application in this manner. For example:
In the end, we have a user experience that is deemed as "reactive". From the perspective of our user, the item has been deleted immediately. Most of the time, we expect our endpoints to just work, so this is perfect. When it fails, we still have some control over how our application will react, because we have successfully separated the concern of the state of our front-end application, with the actual data.
You don't always need a store, mind you. If you find that you are writing stores that look like this:
export default {
state: {
orders: []
},
mutations: {
ADD_ORDER (state, order) {
state.orders.push(order)
},
DELETE_ORDER (state, orderToDelete) {
state.orders = state.orders.filter(order => order.id !== orderToDelete.id)
}
},
actions: {
addOrder ({commit}, order) {
commit('ADD_ORDER', order)
},
deleteOrder ({commit}, order) {
commit('DELETE_ORDER', order)
}
},
getters: {
orders: state => state.orders
}
}
To me it seems you are only using the store as a data store, and are perhaps missing out on the reactivity aspect of it, by not letting it also take control of variables that your application reacts to. Basically, you can and should probably offload some lines of code written in your components to your stores.
Because there’s no state without mutations! When commited — a piece of logic, that changes the state in a foreseeable manner, is executed. Mutations are the only way to set or change the state (so there’s no direct changes!), and furthermore — they must be synchronous. This solution drives a very important functionality: mutations are logging into devtools. And that provides you with a great readability and predictability!
One more thing — actions. As it’s been said — actions commit mutations. So they do not change the store, and there’s no need for these to be synchronous. But, they can manage an extra piece of asynchronous logic!