Actions are functions that dispatch mutations. Actions can be asynchronous and a single action can dispatch multiple mutations.
An action expresses the intention for something to happen, and abstracts the details away from the component calling it. When a component wants to do something, it just calls an action - there's no need to worry about a callback or a return value, because actions result in state changes, and state changes will trigger the component's DOM to update - the component is completely decoupled from how that action is actually performed.
Vuex actions are in fact "action creators" in vanilla flux definitions, but I find that term more confusing than useful.
It is common that an action simply triggers a single mutation. Vuex provides a shorthand for defining such actions:
const vuex = new Vuex({
state: {
count: 1
},
mutations: {
INCREMENT (state, x) {
state += x
}
},
actions: {
// shorthand
// just provide the mutation name.
increment: 'INCREMENT'
}
})
Now when we call the action:
vuex.actions.increment(1)
It simply calls the following for us:
vuex.dispatch('INCREMENT', 1)
Note any arguments passed to the action is also passed along to the mutation handler.
How about actions that involve logic depending on current state, or that need async operations? We can define these actions as thunks - essentially functions that return another function:
const vuex = new Vuex({
state: {
count: 1
},
mutations: {
INCREMENT (state, x) {
state += x
}
},
actions: {
incrementIfOdd: function (x) {
return function (dispatch, state) {
if ((state.count + 1) % 2 === 0) {
dispatch('INCREMENT', x)
}
}
}
}
})
Here, the outer function receives the arguments passed when calling the action. Then, it returns a function that gets 2 arguments: first is the dispatch
function, and the second being the state
. We are using ES5 syntax here to make things easier to understand. With ES2015 arrow functions we can "prettify" the above to the following:
// ...
actions: {
incrementIfOdd: x => (dispatch, state) => {
if ((state.count + 1) % 2 === 0) {
dispatch('INCREMENT', x)
}
}
}
The string shorthand is essentially syntax sugar for the following:
actions: {
increment: 'INCREMENT'
}
// ... equivalent to:
actions: {
increment: (...args) => dispatch => dispatch('INCREMENT', ...args)
}
Why don't we just define the actions as simple functions that directly access vuex.state
and vuex.dispatch
? The reason is that couples the action functions to the specific vuex instance. By using the thunk syntax, our actions only depend on function arguments and nothing else - this important characteristic makes them easy to test and hot-reloadable!
We can use the same thunk syntax for defining async actions:
// ...
actions: {
incrementAsync: x => dispatch => {
setTimeout(() => {
dispatch('INCREMENT', x)
}, 1000)
}
}
A more practical example is when checking out a shopping cart - we may need to trigger multiple mutations: one that signifies the checkout has started, one for success, and one for failure:
// ...
actions: {
checkout: products => (dispatch, state) => {
// save the current in cart items
const savedCartItems = [...state.cart.added]
// send out checkout request, and optimistically
// clear the cart
dispatch(types.CHECKOUT_REQUEST)
// the shop API accepts a success callback and a failure callback
shop.buyProducts(
products,
// handle success
() => dispatch(types.CHECKOUT_SUCCESS),
// handle failure
() => dispatch(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
Again, all the component needs to do to perform the entire checkout is just calling vuex.actions.checkout(products)
.