Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vuex Action vs Mutations

Tags:

vue.js

vuex

People also ask

What is the difference between Vuex mutations and actions?

Vuex mutations are essentially events: each mutation has a name and a handler. Actions: Actions are just functions that dispatch mutations.

What are mutations in Vuex?

In Vuex, mutations are synchronous transactions: store. commit('increment') // any state change that the "increment" mutation may cause // should be done at this moment. To handle asynchronous operations, let's introduce Actions.

Can I call action from mutation Vuex?

In Vuex, actions are functions that call mutations. Actions exist because mutations must be synchronous, whereas actions can be asynchronous. You can define actions by passing a POJO as the actions property to the Vuex store constructor as shown below. To "call" an action, you should use the Store#dispatch() function.

Are Vuex actions async?

Dispatching Actions Note we are performing a flow of asynchronous operations, and recording the side effects (state mutations) of the action by committing them.


Question 1: Why the Vuejs developers decided to do it this way?

Answer:

  1. When your application becomes large, and when there are multiple developers working on this project, you will find the "state manage" (especially the "global state"), will become increasingly more complicated.
  2. The vuex way (just like Redux in react.js) offers a new mechanism to manage state, keep state, and "save and trackable" (that means every action which modifies state can be tracked by debug tool:vue-devtools)

Question 2: What's the difference between "action" and "mutation"?

Let's see the official explanation first:

Mutations:

Vuex mutations are essentially events: each mutation has a name and a handler.

import Vuex from 'vuex'

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    INCREMENT (state) {
      // mutate state
      state.count++
    }
  }
})

Actions: Actions are just functions that dispatch mutations.

// the simplest action
function increment ({commit}) {
  commit('INCREMENT')
}

// a action with additional arguments
// with ES2015 argument destructuring
function incrementBy ({ dispatch }, amount) {
  dispatch('INCREMENT', amount)
}

Here is my explanation of the above:

  • mutation is the only way to modify state
  • mutation doesn't care about business logic, it just cares about "state"
  • action is business logic
  • action can commit more than 1 mutation at a time, it just implements the business logic, it doesn't care about data changing (which manage by mutation)

Mutations are synchronous, whereas actions can be asynchronous.

To put it in another way: you don't need actions if your operations are synchronous, otherwise implement them.


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.


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.


The main differences between Actions and Mutations:

  1. In mutations you can change the state but not it actions.
  2. Inside actions you can run asynchronous code but not in mutations.
  3. Inside actions you can access getters, state, mutations (committing them), actions (dispatching them) etc in mutations you can access only the state.

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:

  • The store has an internal state, which should never be directly accessed by components (mapState is effectively banned)
  • The store has mutations, which are synchronous modification to the internal state. A mutation's only job is to modify the state. They should only be called from an action. They should be named to describe things that happened to the state (ORDER_CANCELED, ORDER_CREATED). Keep them short and sweet. You can step through them by using the Vue Devtools browser extension (it's great for debugging too!)
  • The store also has actions, which should be async or return a promise. They are the actions that your components will call when they will want to modify the state of the application. They should be named with business oriented actions (verbs, ie cancelOrder, createOrder). This is where you validate and send your requests. Each action may call different commits at different step if it is required to change the state.
  • Finally, the store has getters, which are what you use to expose your state to your components. Expect them to be heavily used across many components as your application expands. Vuex caches getters heavily to avoid useless computation cycles (as long as you don't add parameters to your getter - try not to use parameters) so don't hesitate to use them extensively. Just make sure you give names that describe as close as possible what state the application currently is in.

That being said, the magic begins when we start designing our application in this manner. For example:

  • We have a component that offers a list of orders to the user with the possibility to delete those orders
  • The components has mapped a store getter (deletableOrders), which is an array of objects with ids
  • The component has a button on each row of orders, and its click is mapped to a store action (deleteOrder) which passes the order object to it (which, we will remember, comes from the store's list itself)
  • The store deleteOrder action does the following:
    • it validates the deletion
    • it stores the order to delete temporarily
    • it commits the ORDER_DELETED mutation with the order
    • it sends the API call to actually delete the order (yes, AFTER modifying the state!)
    • it waits for the call to end (the state is already updated) and on failure, we call the ORDER_DELETE_FAILED mutation with the order we kept earlier.
  • The ORDER_DELETED mutation will simply remove the given order from the list of deletable orders (which will update the getter)
  • The ORDER_DELETE_FAILED mutation simply puts it back, and modifies to state to notify of the error (another component, error-notification, would be tracking that state to know when to display itself)

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.


According to the docs

Actions are similar to mutations, the differences being that:

  • Instead of mutating the state, actions commit mutations.
  • Actions can contain arbitrary asynchronous operations.

Consider the following snippet.

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++               //Mutating the state. Must be synchronous
    }
  },
  actions: {
    increment (context) {
      context.commit('increment') //Committing the mutations. Can be asynchronous.
    }
  }
})

Action handlers(increment) receive a context object which exposes the same set of methods/properties on the store instance, so you can call context.commit to commit a mutation, or access the state and getters via context.state and context.getters