NexusCS

Vuex

JavaScript libraries
Quick reference for Vuex, the state management library for Vue.js. Covers Vuex 4 (Vue 3). Note: Pinia is now recommended for new projects.
wip

Getting started

Store Creation (Vuex 4)

import { createApp } from "vue";
import { createStore } from "vuex";

const store = createStore({
  state() {
    return { count: 0 };
  },
  mutations: {
    increment(state) {
      state.count++;
    },
  },
});

const app = createApp({
  /* root */
});
app.use(store);

Accessing State

computed: {
  count () {
    return this.$store.state.count
  }
}

Direct access in components.

Composition API

import { computed } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    return {
      count: computed(() => store.state.count),
      increment: () => store.commit("increment"),
    };
  },
};

Vue 3 composition API pattern.

State

mapState Helper (Object)

import { mapState } from "vuex";

export default {
  computed: mapState({
    count: (state) => state.count,
    countAlias: "count",
    countPlusLocal(state) {
      return state.count + this.localCount;
    },
  }),
};

Map state to computed properties.

mapState Helper (Array)

computed: mapState(["count", "todos"]);

Simple string array mapping.

Spread Operator

computed: {
  localComputed () { /* ... */ },
  ...mapState({ count: 'count' })
}

Mix with local computed properties.

Getters

Defining Getters

getters: {
  doneTodos (state) {
    return state.todos.filter(todo => todo.done)
  },
  doneTodosCount (state, getters) {
    return getters.doneTodos.length
  }
}

Computed derived state.

Property-Style Access

store.getters.doneTodos;
this.$store.getters.doneTodosCount;

Cached like computed properties.

Method-Style Access

getters: {
  getTodoById: (state) => (id) => {
    return state.todos.find((todo) => todo.id === id);
  };
}

store.getters.getTodoById(2);

Not cached, runs every call.

mapGetters Helper

import { mapGetters } from 'vuex'

computed: {
  ...mapGetters([
    'doneTodosCount',
    'anotherGetter'
  ]),
  ...mapGetters({
    doneCount: 'doneTodosCount'
  })
}

Map to component computed properties.

Mutations

Basic Mutations

mutations: {
  increment (state) {
    state.count++
  }
}

store.commit('increment')

Synchronous state changes only.

Mutations with Payload

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

store.commit('increment', { amount: 10 })

Pass data to mutations.

Object-Style Commit

store.commit({
  type: "increment",
  amount: 10,
});

Entire object as payload.

Mutation Type Constants

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'

// store.js
import { SOME_MUTATION } from './mutation-types'

mutations: {
  [SOME_MUTATION] (state) { /* ... */ }
}

Type safety with constants.

mapMutations Helper

import { mapMutations } from 'vuex'

methods: {
  ...mapMutations([
    'increment',
    'incrementBy'
  ]),
  ...mapMutations({
    add: 'increment'
  })
}

Map to component methods.

Actions

Defining Actions

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

Asynchronous operations allowed.

Context Object

actions: {
  someAction (context) {
    context.commit('mutation')
    context.dispatch('action')
    context.state.count
    context.getters.done
  }
}

Full store access via context.

Dispatching Actions

store.dispatch("increment");
store.dispatch("incrementAsync", { amount: 10 });
store.dispatch({
  type: "incrementAsync",
  amount: 10,
});

Call actions from components.

Async Actions (Promises)

actions: {
  actionA ({ commit }) {
    return new Promise((resolve) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

store.dispatch('actionA').then(() => {
  // ...
})

Return promises for chaining.

Async/Await

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA')
    commit('gotOtherData', await getOtherData())
  }
}

Modern async syntax.

mapActions Helper

import { mapActions } from 'vuex'

methods: {
  ...mapActions([
    'increment',
    'incrementBy'
  ]),
  ...mapActions({
    add: 'increment'
  })
}

Map to component methods.

Modules

Basic Module Structure

const moduleA = {
  state: () => ({ count: 0 }),
  mutations: {
    /* ... */
  },
  actions: {
    /* ... */
  },
  getters: {
    /* ... */
  },
};

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB,
  },
});

store.state.a; // -> moduleA's state

Split store into modules.

Accessing Root State

actions: {
  someAction ({ state, rootState }) {
    if ((state.count + rootState.count) % 2 === 1) {
      commit('increment')
    }
  }
},
getters: {
  sumWithRootCount (state, getters, rootState) {
    return state.count + rootState.count
  }
}

Access from module context.

Namespaced Modules

const store = createStore({
  modules: {
    account: {
      namespaced: true,
      state: () => ({
        /* ... */
      }),
      getters: {
        isAdmin() {
          /* ... */
        },
      },
      actions: {
        login() {
          /* ... */
        },
      },
      mutations: {
        login() {
          /* ... */
        },
      },
    },
  },
});

// Access:
// getters['account/isAdmin']
// dispatch('account/login')
// commit('account/login')

Prevent naming collisions.

Global Assets from Namespaced

actions: {
  someAction ({ dispatch, commit }) {
    dispatch('someOtherAction', null, { root: true })
    commit('someMutation', null, { root: true })
  }
}

Access global from namespaced module.

Helpers with Namespace

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a
  }),
  ...mapGetters('some/nested/module', [
    'someGetter'
  ])
},
methods: {
  ...mapActions('some/nested/module', [
    'foo',
    'bar'
  ])
}

Namespace string as first argument.

createNamespacedHelpers

import { createNamespacedHelpers } from "vuex";

const { mapState, mapActions } = createNamespacedHelpers("some/nested/module");

export default {
  computed: {
    ...mapState({ a: (state) => state.a }),
  },
  methods: {
    ...mapActions(["foo", "bar"]),
  },
};

Avoid repetitive namespace strings.

Dynamic Modules

store.registerModule("myModule", {
  /* ... */
});

store.registerModule(["nested", "myModule"], {
  /* ... */
});

store.unregisterModule("myModule");

store.hasModule("myModule"); // -> boolean

Add/remove modules at runtime.

Store API

Instance Methods

store.commit("mutation", payload);
store.dispatch("action", payload);
store.replaceState(newState);

Core store methods.

Watch State

const unwatch = store.watch(
  (state, getters) => state.count,
  (newVal, oldVal) => {
    // React to changes
  },
);

Watch state changes.

Subscribe to Mutations

const unsub = store.subscribe((mutation, state) => {
  console.log(mutation.type);
  console.log(mutation.payload);
});

Called after every mutation.

Subscribe to Actions

const unsubAction = store.subscribeAction({
  before: (action, state) => {},
  after: (action, state) => {},
  error: (action, state, error) => {},
});

Hook into action lifecycle.

Hot Module Replacement

store.hotUpdate({
  mutations,
  modules: {
    a: newModuleA,
  },
});

Update without full reload.

Plugins

Custom Plugin

const myPlugin = (store) => {
  store.subscribe((mutation, state) => {
    // Called after every mutation
  });
};

const store = createStore({
  plugins: [myPlugin],
});

Extend Vuex functionality.

Built-in Logger

import { createLogger } from "vuex";

const store = createStore({
  plugins: [
    createLogger({
      collapsed: false,
      filter(mutation, stateBefore, stateAfter) {
        return mutation.type !== "blocklisted";
      },
      logActions: true,
      logMutations: true,
    }),
  ],
});

Development debugging tool.

Advanced

Strict Mode

const store = createStore({
  strict: process.env.NODE_ENV !== "production",
});

Detect mutations outside handlers.

Form Handling

<input v-model="message">

computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

Two-way computed for strict mode.

Gotchas

Mutations Must Be Synchronous

Async callbacks in mutations are un-trackable by devtools. Use actions for async operations.

// ❌ Bad
mutations: {
  someMutation (state) {
    setTimeout(() => {
      state.count++
    }, 1000)
  }
}

// ✅ Good
actions: {
  someAction ({ commit }) {
    setTimeout(() => {
      commit('someMutation')
    }, 1000)
  }
}

Module State Must Be Function

Plain objects cause shared references between instances.

// ❌ Bad
state: {
  count: 0;
}

// ✅ Good
state: () => ({ count: 0 });

Strict Mode in Production

Deep-watches entire state tree. Only enable in development.

strict: process.env.NODE_ENV !== "production";

v-model in Strict Mode

Direct v-model="$store.state.x" throws errors in strict mode.

// Use computed getter/setter instead
computed: {
  message: {
    get () { return this.$store.state.message },
    set (value) { this.$store.commit('updateMessage', value) }
  }
}

Method-Style Getters Not Cached

Getters returning functions run every time called.

// Not cached
getTodoById: (state) => (id) => {
  return state.todos.find((todo) => todo.id === id);
};

Namespace Collisions

Non-namespaced modules with same getter/mutation names cause conflicts.

// Use namespaced: true
const module = {
  namespaced: true,
  // ...
};

Also see