State Management in Vue.js Using Vuex

State

The centralized store instance is injected into all child components, providing reactive access to the state object. For instance, defining an inventory property:

export default new Vuex.Store({
  state: {
    inventory: 50
  }
})

Components can retrieve this value via computed properties:

export default {
  computed: {
    stockLevel() {
      return this.$store.state.inventory;
    }
  }
}

mapState

When a component needs to access multiple state properties, declaring individual computed properties becomes verbose. The mapState helper streamlines this process.

import { mapState } from 'vuex';

Using the spread operator allows combining local computed properties with mapped state properties seamlessly:

computed: {
  localCalc() {
    return this.baseValue * 2;
  },
  ...mapState({
    stock: 'inventory',
    combinedStock(state) {
      return state.inventory + this.warehouseReserve;
    }
  })
}

Getters

Getters act as computed properties for the store. They are useful for deriving or filtering state before returning it to components. Consider a state containing an array of products:

state: {
  products: [
    { id: 1, name: 'Laptop', inStock: true },
    { id: 2, name: 'Mouse', inStock: false },
    { id: 3, name: 'Keyboard', inStock: true }
  ]
}

To filter available products and calculate their count, define getters:

getters: {
  availableItems(state) {
    return state.products.filter(item => item.inStock);
  },
  availableCount(state, getters) {
    return getters.availableItems.length;
  }
}

mapGetters

The mapGetters helper maps store getters to local computed properties:

import { mapGetters } from 'vuex';

computed: {
  localProp() {
    return 'Some local data';
  },
  ...mapGetters([
    'availableItems'
  ]),
  ...mapGetters({
    itemCount: 'availableCount'
  })
}

Mutations

State modifications must be performed within mutations to ensure traceability. Mutations are synchronous functions that receive the state as the first argument.

mutations: {
  RESTOCK(state) {
    state.inventory++;
  },
  DEPLETION(state) {
    state.inventory--;
  }
}

Components trigger mutations using commit:

methods: {
  addItem() {
    this.$store.commit('RESTOCK');
  }
}

Payloads

Additional data can be passed to mutations via a payload. It is recommended to use objects for payloads to improve readability:

mutations: {
  SET_INVENTORY(state, payload) {
    state.inventory += payload.amount;
  }
}

// Component invocation
this.$store.commit('SET_INVENTORY', { amount: 20 });

// Object-style commit
this.$store.commit({ type: 'SET_INVENTORY', amount: 20 });

Reactivity Rules

When adding new properties to state objects, Vue's reactivity system requires specific handling. Always initialize state properties upfront. For new properties, use Vue.set() or replace the entire object using the spread operator:

mutations: {
  ADD_DISCOUNT(state) {
    // Vue.set(state, 'discountTag', 'SALE');
    state.config = { ...state.config, discountTag: 'SALE' };
  }
}

Synchronous Requirement

Mutations must remain synchronous. Asynchronous logic inside a mutation breaks state tracking:

// Incorrect approach
mutations: {
  BAD_MUTATION(state) {
    api.callAsync(() => {
      state.inventory++;
    });
  }
}

mapMutations

Mapping mutations simplifies component methods:

import { mapMutations } from 'vuex';

methods: {
  ...mapMutations([
    'RESTOCK', // maps to this.$store.commit('RESTOCK')
    'SET_INVENTORY' // maps to this.$store.commit('SET_INVENTORY', payload)
  ]),
  ...mapMutations({
    deplete: 'DEPLETION' // maps to this.$store.commit('DEPLETION')
  })
}

Actions

Actions handle asynchronous operations. Instead of mutating state directly, they commit mutations. An action receives a context object exposing the same methods and properties as the store instance.

state: {
  shipmentStatus: 'pending'
},
mutations: {
  UPDATE_STATUS(state, payload) {
    state.shipmentStatus = payload.status;
  }
},
actions: {
  processShipment(context, payload) {
    const finalStatus = 'shipped-' + payload.priority;
    setTimeout(() => {
      context.commit('UPDATE_STATUS', { status: finalStatus });
    }, 1500);
  }
}

Components dispatch actions:

methods: {
  shipOrder() {
    this.$store.dispatch('processShipment', { priority: 'express' });
    // Object-style dispatch
    this.$store.dispatch({ type: 'processShipment', priority: 'standard' });
  }
}

Composing Actions with Promises

Actions naturally return Promises, enabling sequential execution:

actions: {
  actionA({ commit }) {
    return new Promise((resolve) => {
      setTimeout(() => {
        commit('SOME_MUTATION');
        resolve();
      }, 1000);
    });
  },
  actionB({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('ANOTHER_MUTATION');
    });
  }
}

mapActions

import { mapActions } from 'vuex';

methods: {
  ...mapActions([
    'processShipment' // maps to this.$store.dispatch('processShipment')
  ]),
  ...mapActions({
    ship: 'processShipment' // maps to this.$store.dispatch('processShipment')
  })
}

Modules

To prevent a monolithic store, Vuex allows dividing the store into modules. Each module possesses its own state, mutations, actions, and getters.

const userModule = {
  state: { username: 'admin', role: 'super' },
  mutations: { /* ... */ }
};

const productModule = {
  state: { itemName: 'Widget', price: 9.99 },
  mutations: { /* ... */ }
};

export default new Vuex.Store({
  modules: {
    user: userModule,
    product: productModule
  }
});

Module state is accessed based on the registration key:

computed: {
  currentUser() {
    return this.$store.state.user.username;
  }
}

Namespaced Modules

By default, mutations, actions, and getters are registered globally. Adding namespaced: true isolates them based on their module path.

const cartModule = {
  namespaced: true,
  state: { items: [10, 20, 30, 40] },
  getters: {
    premiumItems(state) {
      return state.items.filter(i => i > 25);
    }
  },
  mutations: {
    ADD_ITEM(state, payload) {
      state.items.push(payload.val);
    }
  },
  actions: {
    // context.state and context.rootState available
  }
};

export default new Vuex.Store({
  modules: { cart: cartModule }
});

Namespaced mutations require the path prefix:

this.$store.commit('cart/ADD_ITEM', { val: 50 });

Accessing Global Assets in Namespaced Modules

Within namespaced modules, getters receive rootState and rootGetters as third and fourth arguments. Actions receive rootGetters and can dispatch or commit globally using { root: true }.

const nestedModule = {
  namespaced: true,
  getters: {
    combinedGetter(state, getters, rootState, rootGetters) {
      return state.value + rootState.globalValue;
    }
  },
  actions: {
    globalAction({ dispatch, commit }) {
      dispatch('someGlobalAction', null, { root: true });
      commit('SOME_GLOBAL_MUTATION', null, { root: true });
    }
  }
};

Namespaced Module Helpers

When using mapping helpers with namespaced modules, pass the namespace string as the first argument:

computed: {
  ...mapState('cart', ['items'])
},
methods: {
  ...mapMutations('cart', ['ADD_ITEM'])
}

Alternatively, use createNamespacedHelpers to generate bound helpers:

import { createNamespacedHelpers } from 'vuex';
const { mapState, mapMutations } = createNamespacedHelpers('cart');

Tags: Vuex Vue.js State Management frontend javascript

Posted on Thu, 20 Aug 2026 16:24:00 +0000 by MrKaraokeSteve