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
- Vuex Official Documentation - Complete Vuex guide
- Vuex API Reference - API documentation
- Vuex GitHub Repository - Source code
- Pinia Official Documentation - Recommended successor for new projects
- Vue 3 Composition API - Composition API guide