Vuex is the official state management library for Vue (before Pinia replaced it). It implements the Flux pattern: a single store holds all shared state, and changes flow in one direction through a strict pipeline.
Note: Vuex is in maintenance mode. For new projects, use Pinia. It's the officially recommended store for Vue 3. Understanding Vuex still matters because many existing apps use it.
The four pieces
Every Vuex store has four concepts, each with a specific role:
Component → dispatch(Action) → commit(Mutation) → State → Getters → ComponentState
The single source of truth: a reactive object that holds all shared data. Components read from it, but never write to it directly.
const store = createStore({
state: {
users: [] as User[],
loading: false
}
})Getters
Computed values derived from state. They cache their result and only recalculate when the underlying state changes, the same concept as computed() in a component.
getters: {
activeUsers: (state) => state.users.filter(u => u.isActive),
userCount: (state, getters) => getters.activeUsers.length
}Mutations
The ONLY way to change state. Mutations must be synchronous. This is the key constraint. It ensures every state change is trackable in Vue DevTools (you can see exactly when and how state changed).
mutations: {
SET_USERS(state, users: User[]) {
state.users = users
},
SET_LOADING(state, value: boolean) {
state.loading = value
}
}Actions
Where async logic lives. Actions don't change state directly; they call mutations via commit(). This separation is why Vuex can track every state change: mutations are the bottleneck, and they're always synchronous.
actions: {
async fetchUsers({ commit }) {
commit('SET_LOADING', true)
const response = await fetch('/api/users')
const users = await response.json()
commit('SET_USERS', users)
commit('SET_LOADING', false)
}
}Using the store in a component
<script setup lang="ts">
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const users = computed(() => store.state.users)
const activeUsers = computed(() => store.getters.activeUsers)
function loadUsers() {
store.dispatch('fetchUsers')
}
</script>Notice the string-based API: commit('SET_USERS'), dispatch('fetchUsers'), store.getters.activeUsers. These strings aren't type-safe. Typos become runtime bugs, not compile errors. This is one of the main reasons Pinia was created.
Modules
Large apps split the store into modules, each with its own state, mutations, actions, and getters. Namespaced modules prevent naming collisions:
const userModule = {
namespaced: true,
state: () => ({ list: [] as User[] }),
mutations: {
SET_LIST(state, users: User[]) {
state.list = users
}
}
}
// Access: store.commit('user/SET_LIST', users)Why Pinia replaced Vuex
Vuex's design (especially the mutations layer and string-based API) was created before TypeScript was common in Vue projects. Pinia solves the pain points: full TypeScript inference, no mutations (actions change state directly), no string keys, no namespaced modules. If you're starting fresh, use Pinia.
See also: How does Pinia work? · What is the Flux / unidirectional data flow pattern? · How do you test a Pinia store?
References
- What is Vuex? - Vuex docs
- Getting Started - Vuex docs
- Modules - Vuex docs