Can I call commit from one of mutations in Vuex store
Asked Answered
I

16

119

I have a vuex store, like following:

import spreeApi from '../../gateways/spree-api'
// initial state
const state = {
  products: [],
  categories: []
}

// mutations
const mutations = {
 SET_PRODUCTS: (state, response) => {
   state.products = response.data.products
   commit('SET_CATEGORIES')
 },
 SET_CATEGORIES: (state) => {
   state.categories = state.products.map(function(product) { return product.category})
 }

}

const actions = {
 FETCH_PRODUCTS: (state, filters) => {
   return spreeApi.get('products').then(response => state.commit('SET_PRODUCTS', response))
 }
}

export default {
  state,
  mutations,
  actions
}

I want to call mutation: SET_CATEGORIES from mutation: SET_PRODUCTS, But this gives me error:

projectFilter.js:22 Uncaught (in promise) ReferenceError: commit is not defined(…)

What should be correct way to do this. I tried store.commit and this.commit, but these also gave similar errors.

Illuminative answered 8/11, 2016 at 13:1 Comment(3)
Related (closed) issue: github.com/vuejs/vuex/issues/907Rebound
Hi @Saurabh, I have tested Kubiwama Adrien's answer, and it seems that it has what you need, maybe test it and update this forum with the latest answer? Thanks!Adversative
Why not use an action and call multiple mutations inside it?Detinue
S
65

When you are already doing a mutation, there is no way to commit another mutation. A mutation is a synchronous call which changes the state. Within one mutation, you will not be able to commit another mutation.

Here is the API reference for Vuex: https://vuex.vuejs.org/en/api.html

As you can see, a mutation handler receives only state and payload, nothing more. Therefore you are getting commit as undefined.

In your case above, you can set the PRODUCT and CATEGORIES as part of the same mutation handler as a single commit. You can try if the following code works:

// mutations
const mutations = {
    SET_PRODUCTS_AND_CATEGORIES: (state, response) => {
        state.products = response.data.products
        state.categories = state.products.map(function(product) { return product.category})
    },
    // ...
}

EDIT: Please refer to the answer below, provided by Daniel S. Deboer. The correct method is to commit two mutations from a single action, as described in his answer.

Speechless answered 8/11, 2016 at 14:24 Comment(7)
I'm wondering why it is not permitted to commit a mutation from another mutation? Some mutations can be so atomic that they should be used by larger mutation functions. In the current version, there should be much duplication of code due to this restriction.Paresis
Yes, it is possible to have a commit from another mutation with no side-effects. But debugging an incorrect state change when we have mutations calling each other is going to lead to a very bad developer experience. I think that is why it is not recommended or allowed. But thankfully we have this simple solution - we can always define a single mutation that does multiple state changes, as seen in this question / answer.Speechless
IMHO, the answer is below (commit two things from one action). It's cleaner and more readable.Chemush
@JCKödel I agree, the correct answer should be the one below (commit two mutations from the action), provided by Daniel. I will put a note in my answer above, to refer to the better answer below.Speechless
@Mani why this is not preferred over Dani's answer? I thought this was semantically more correct from Vuex standpoint since action is designed primarily for async operations.Emit
@AlexNapitupulu Usually we have a mutation handler doing only one simple and straightforward thing at a time - either set products or set categories, not both. Based on the requirements of this question, we need to do two things at the same time. It is better to push this complexity into an action rather than a mutation. Mutations are more internal to the store and closer to the actual data, while actions are the ones that are exposed to components. So it is logical to have an action that does what the component specifically wants.Speechless
@AlexNapitupulu Regarding async operations, both these approaches are equivalent as the action in its entirety is asynchronous. Once the action is initiated asynchronously, it does not matter whether it commits two mutations one after another or calls a special mutation handler that does two things in the store. It is a design choice, but I think it is best to keep this complexity closer to the component (in the action) than handle it further down in a mutation.Speechless
S
155

If you absolutely must commit two mutations, why not do it from an action? Actions don't have to perform async operations. You can destructure the commit method in your action the same way you do with state like so:

commitTwoThings: ({commit}, payload) => {
  commit('MUTATION_1', payload.thing)
  commit('MUTATION_2', payload.otherThing)
}
Spithead answered 31/3, 2017 at 19:5 Comment(1)
Why not ?.. With vue-native-websocke you need to handle data from a ws server in a mutation (SOCKET_ONMESSAGE)... And their is no way to call an action from a mutation neither.Saltation
B
142

For the record. To call other mutations from a mutation method do it like this:

const mutations = {
    mutationOne(state, payload){
        this.commit("mutationTwo", payload)
    },
    mutationTwo(state, payload){
        console.log("called from another mutation", payload)
    }
}
Bivouac answered 22/2, 2019 at 15:35 Comment(6)
Is this a new(ish) feature of Vuex? Surprised that it took two and a half years for someone to point out the clearly correct answer.Nonentity
I'm not quite sure if this is considered as a best practice. However, this answer has directly answered the question. without providing alternatives that has been suggested by others via action. I have tested it and it seems to work well. I think this answer should be on top instead. This solution has already been discussed in Vuex Github - Please add an ability to call mutation from another mutation #907 if anyone interested in reading more proof.Adversative
By the way, if your modules is namespaced, even though it's under the same file you have to access it via this.commit('modulesName/mutationName') just in case if anyone was wondering. If you need more info, it's always a good reminder to just do console.log(this) inside the mutation, it seems that it holds the same instance as Vue, which you can also access $route too from there.Adversative
Also, this.commit only works if mutations are not arrow functions. mutationOne(state, payload){ vs mutationOne: (state, payload) => { because if they are arrow functions, then 'this' is undefined.Centistere
@IrfandyJip (and other passers-by): I've had a good long look at this, and I disagree that this answer should be on top. Having read the referenced git-hub thread, it seems to conclude that using actions{} is the correct answer (mainly because of how Vuex logs state changes). More fundamentally though, I think it makes logical sense to structure code in such a way that mutations only manage state changes and actions manage a flow of state-changes (i.e. some sort of higher level "feature" of the overall application).Jonellejones
@IrfandyJip One could argue that 3 different properties within the state are functionally linked together, and that therefore they should all be changed within the same mutation. Okay, that's fine. But why not just directly access/update those three state properties within the mutation. No need to commit them via other mutations. On the other hand, if these 3 properties are only occasionally related (and therefore don't fit neatly into a single mutation), then I'd point out that your scenario is more of a "process flow" relationship, and that you should revert back to actions.Jonellejones
S
65

When you are already doing a mutation, there is no way to commit another mutation. A mutation is a synchronous call which changes the state. Within one mutation, you will not be able to commit another mutation.

Here is the API reference for Vuex: https://vuex.vuejs.org/en/api.html

As you can see, a mutation handler receives only state and payload, nothing more. Therefore you are getting commit as undefined.

In your case above, you can set the PRODUCT and CATEGORIES as part of the same mutation handler as a single commit. You can try if the following code works:

// mutations
const mutations = {
    SET_PRODUCTS_AND_CATEGORIES: (state, response) => {
        state.products = response.data.products
        state.categories = state.products.map(function(product) { return product.category})
    },
    // ...
}

EDIT: Please refer to the answer below, provided by Daniel S. Deboer. The correct method is to commit two mutations from a single action, as described in his answer.

Speechless answered 8/11, 2016 at 14:24 Comment(7)
I'm wondering why it is not permitted to commit a mutation from another mutation? Some mutations can be so atomic that they should be used by larger mutation functions. In the current version, there should be much duplication of code due to this restriction.Paresis
Yes, it is possible to have a commit from another mutation with no side-effects. But debugging an incorrect state change when we have mutations calling each other is going to lead to a very bad developer experience. I think that is why it is not recommended or allowed. But thankfully we have this simple solution - we can always define a single mutation that does multiple state changes, as seen in this question / answer.Speechless
IMHO, the answer is below (commit two things from one action). It's cleaner and more readable.Chemush
@JCKödel I agree, the correct answer should be the one below (commit two mutations from the action), provided by Daniel. I will put a note in my answer above, to refer to the better answer below.Speechless
@Mani why this is not preferred over Dani's answer? I thought this was semantically more correct from Vuex standpoint since action is designed primarily for async operations.Emit
@AlexNapitupulu Usually we have a mutation handler doing only one simple and straightforward thing at a time - either set products or set categories, not both. Based on the requirements of this question, we need to do two things at the same time. It is better to push this complexity into an action rather than a mutation. Mutations are more internal to the store and closer to the actual data, while actions are the ones that are exposed to components. So it is logical to have an action that does what the component specifically wants.Speechless
@AlexNapitupulu Regarding async operations, both these approaches are equivalent as the action in its entirety is asynchronous. Once the action is initiated asynchronously, it does not matter whether it commits two mutations one after another or calls a special mutation handler that does two things in the store. It is a design choice, but I think it is best to keep this complexity closer to the component (in the action) than handle it further down in a mutation.Speechless
E
35

To share code between mutations, you must create a new function that performs the work, which you can then reuse. Fortunately, mutations are just plain old functions, and we can pass the state parameter around however we like, so this is quite easy to do.

For example:

const mutations = {
 SET_PRODUCTS: (state, response) => {
   state.products = response.data.products
   setCategories(state)
 },
 SET_CATEGORIES: (state) => {
   setCategories(state)
 }
}

function setCategories(state) {
  state.categories = state.products.map(product => product.category)
}
Ecto answered 25/2, 2017 at 6:23 Comment(4)
I think this gives an error about chaining state outside of a mutationNormalie
What makes you think that? As long as you only call setCategories from within a mutation, it'll be fine.Ecto
This worked for me, but I left the function implementation as a mutation and just called mutations.setCategories(state) from the function.Hoop
unfortunately in the Typescript Vue approach this doesn't work, bc. of not being able to access/see the plain functionsSprouse
G
18

And if I have some common code that affects state between multiple mutations, I have to duplicate the same code on all my mutations? Or there's a better way to do that?

Green answered 5/12, 2016 at 20:39 Comment(7)
The silence is deafening. I'd love an answer to this too.Phenylalanine
@Green see the answer I just created. It's not ideal but it's the best I know ofEcto
I agree with Daniel. Functions are the way to re-use code.Plurality
And wrap them in a mutation-helpers.js module or use actions. Though in some cases introducing an action is an overkillConlee
Yep - a mutation is just a function that takes the state as an argument and modifies it. You can declare as many helper functions as you like and reuse them.Fredette
Please post an answer only when you have an answer. If you want to ask something, create a new thread.Corn
@Pablo. Nevertheless, this "answer" has 16 upvotes ;) an important annoyance is addressed in itSprouse
D
9

Reading the Vuex documentation on Actions, it's quite clear what they are made for.

  • commit mutations instead of mutating the state
  • can contain arbitrary asynchronous operations

Actions can (not must) contain asynchronous code. In fact, the following example is correct

increment (context) {
   context.commit('increment')
}

I do not see any issue in using actions for performing multiple mutations.

Devon answered 25/1, 2018 at 16:50 Comment(0)
C
4

In your case you should consider having only one mutation, namely SET_PRODUCTS.

// mutations
const mutations = {
 SET_PRODUCTS: (state, response) => {
   state.products = response.data.products
   state.categories = state.products.map(function(product) { return product.category})
 }
}

You should never have any need to call SET_CATEGORIES separately. Think about it! Categories can only mutate if products are changed. And products can change only through SET_PRODUCTS.

Chiaroscuro answered 16/3, 2017 at 16:30 Comment(0)
M
3

Edit : I stumbled upon a very similar problem and the solution for me was to use a vuex getter : https://vuex.vuejs.org/en/getters.html
Your categories is actually a "computed" version of your products. Having categories as a getter allows you to keep them in sync with products and avoids duplicating the data in your store.

For the sake of answering the question in the title i leave my original answer.
An alternative to Daniel Buckmaster solution :

const mutations = {
 SET_PRODUCTS: (state, response) => {
   state.products = response.data.products
   this.SET_CATEGORIES(state)
 },
 SET_CATEGORIES: (state) => {
   state.categories = state.products.map(product => product.category)
 }
}

As you can see you could directly call the mutation itself. (as Daniel said, they are just plain functions after all)
I believe that this is a more appropriate answer to the original question : it is an actual way of composing mutations without code duplication or extra functions

Mirnamirror answered 19/12, 2017 at 16:45 Comment(9)
Calling a mutation function is not the same as committing a mutation.Hildegaard
Could you elaborate ? Is that really an issue since we are calling it from another mutation ?Mirnamirror
For example, the handler for registered plugins won't be called for the second mutation. Meaning that the Vue dev tools won't show the second mutation in the list of mutations you can "time travel" to.Hildegaard
But the use case is a single mutation that does two things. I understand your point but in that case you do not want to have a store state that have desynchronised lists of products and categories.Mirnamirror
The question is about committing two distinct mutations. A mutation that does multiple thins is another design problem already addressed in Daniel's answer.Hildegaard
Hm yes that is what i meant by "An alternative to Daniel Buckmaster solution". It is my first StackOverflow answer, maybe i should have commented his solution instead.Mirnamirror
You can't comment on other people's posts yet as it takes 50 rep. An answer is ok in this case, I just happen to not like it. You should leave it for the sake of documenting every possible ways and someone might like it and upvote it (an upvote being 10 points, versus -2 for a dv). Don't give up on SO!Hildegaard
Indeed ! This conversation made me realize that a solution to this problem could also be to have category as a getter. I edited my answer to explain a little bit. Please let me know what you think and if i should describe my reasoning a bit more.Mirnamirror
It was not working for me in browser. Only in unit tests.Bloodsucker
B
3

I prefer to call mutations.SET_CATEGORIES(state) instead of: - calling 2 different commits from an artificial action - or doing commit() inside a mutation as it makes unit testing more difficult.

const mutations = {
 SET_PRODUCTS: (state, response) => {
   state.products = response.data.products
   mutations.SET_CATEGORIES(state)
 },
 SET_CATEGORIES: (state) => {
   state.categories = state.products.map(product => product.category)
 }
}

My opinion is that you don't need to see SET_CATEGORIES in the VueToolbox. The time travel should work anyways. Please, correct me if I'm wrong.

Bloodsucker answered 2/6, 2020 at 13:2 Comment(0)
G
2

First, assign the Vue button to a variable: In main.js:

  export const app = new Vue({  
  router,
  vuetify,
  store,....

Then import the "app" variable to the js file where you define the mutation: In modules.js:

import { app } from "../../main";

You can now use it as "app.$store.commit":

mutations: {
[AUTH_SET_TOKEN]: () => {
app.$store.commit(USER_SUCCESS, params );
},...
Gentoo answered 11/5, 2020 at 15:52 Comment(0)
S
1

i think

calling mutation from another mutation is bad idea because of hard to debug state and components

const mutations = {
    mutationOne(state, payload){
        this.commit("mutationTwo", payload)
    },
    mutationTwo(state, payload){
        console.log("called from another mutation", payload)
    }
}

but you can write simple function and function can reusable

function mysecondfn(state,payload){
{
// do your stuff here
}


const mutations = {
    mutationOne(state, payload){
mysecondfn(state,payload)
     },

}
Saleratus answered 15/7, 2020 at 15:9 Comment(0)
B
1

another solution that works for me:

this._mutations.mutationFunction[0]()
Brost answered 23/4, 2021 at 4:50 Comment(0)
R
0
import spreeApi from '../../gateways/spree-api'
// initial state
const state = {
  products: [],
  categories: []
}

// mutations
const mutations = {
 SET_PRODUCTS: (state, {response,commit}) => { // here you destructure the object passed to the mutation to get the response and also the commit function
   state.products = response.data.products
   commit('SET_CATEGORIES') // now the commit function is available
 },
 SET_CATEGORIES: (state) => {
   state.categories = state.products.map(function(product) { return product.category})
 }

}

const actions = {
 FETCH_PRODUCTS: ({commit}, filters) => { // here you destructure the state to get the commit function
   return spreeApi.get('products').then(response => commit('SET_PRODUCTS', {response,commit})) // here you pass the commit function through an object to 'SET_PRODUCTS' mutation
 }
}

export default {
  state,
  mutations,
  actions
}

This should fix it. You can inject the commit into your mutation from the action so you can commit from your mutation. Hope this helps

Rhodos answered 19/2, 2019 at 15:46 Comment(1)
Please add some explanation to your code: what exactly needs to be changed and why? Keep in mind that the OP should be able to learn from your answerHaematite
M
0

you can access to all vuex

this.app.store.commit("toast/show", {
                   mssg:this.app.i18n.t('order.ordersummary.notifymessage'),
                    type: "danger",
                });

access to $i18n in vuex

this.app.i18n.t('order.ordersummary.notifymessage')
Maros answered 22/1, 2022 at 8:32 Comment(0)
A
0

Use this

const mutations = {
 SET_PRODUCTS: (state, response) => {
   state.products = response.data.products
   this.commit('SET_CATEGORIES')
 },
 SET_CATEGORIES: (state) => {
   setCategories(state)
 }
}
  
Ampere answered 24/2, 2022 at 7:41 Comment(1)
While this code snippet may solve the problem, it doesn't explain why or how it answers the question. Please include an explanation for your code, as that really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion.Yttria
P
0

In Vuex, mutations are synchronous functions that are responsible for modifying the state of the Vuex store. While mutations cannot directly call other mutations, you can achieve the desired behavior by utilizing actions.

Here's an example of how you can call one mutation from another mutation using actions:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    doubleIncrement(state) {
      state.count += 2;
    },
  },
  actions: {
    incrementAndDouble({ commit }) {
      commit('increment');
      commit('doubleIncrement');
    },
  },
});

export default store;


<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="incrementAndDouble">Increment and Double</button>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count']),
  },
  methods: {
    ...mapActions(['incrementAndDouble']),
  },
};
</script>
Ptero answered 20/5, 2023 at 9:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.