Vuex
02 / 02

Modules, Plugins & Pinia Migration

Modules, Plugins & Pinia Migration

Namespaced Modules

const cartModule = {
  namespaced: true,  // required to avoid colliding with another module's
                     // action/mutation/getter of the same name
  state: () => ({ items: [] }),
  mutations: {
    ADD_ITEM(state, item) { state.items.push(item); },
  },
  actions: {
    async checkout({ state, commit }) {
      await api.checkout(state.items);
      commit('CLEAR');
    },
  },
};

const store = createStore({
  modules: { cart: cartModule },
});

// Namespaced dispatch/commit needs the module prefix
store.dispatch('cart/checkout');
store.commit('cart/ADD_ITEM', item);

Plugins

// A plugin receives the store and can subscribe to every mutation —
// used for logging, persistence, or custom devtools integrations
const localStoragePlugin = (store) => {
  store.subscribe((mutation, state) => {
    localStorage.setItem('app-state', JSON.stringify(state));
  });
};

const store = createStore({
  // ...
  plugins: [localStoragePlugin],
});

What Belongs in the Store

Reserve the store for genuinely shared state. Purely local, ephemeral UI state (is this dropdown open) usually belongs in a component's own data/ref instead — routing it through commit/dispatch adds indirection with no sharing benefit when nothing else needs that value.

Migrating to Pinia

// Pinia is the newer, officially recommended replacement — no separate
// "mutations" concept, actions can modify state directly
import { defineStore } from 'pinia';

export const useTodoStore = defineStore('todos', {
  state: () => ({ todos: [], loading: false }),
  getters: {
    completedTodos: (state) => state.todos.filter((t) => t.completed),
  },
  actions: {
    async fetchTodos() {
      this.loading = true;
      this.todos = await api.getTodos();  // direct mutation — no commit() needed
      this.loading = false;
    },
  },
});

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free