State Management in Vue with Vuex
Centralizing shared state across components using Vuex: state, mutations, actions, and getters explained.
When I first started writing component-based UIs with Vue, I got by with $emit and props for a while. That works fine for small apps. But once the number of components grows and unrelated sibling components need to share the same data, that approach hits a wall. Tracking who is sending what to whom becomes genuinely painful.
Vuex is the centralized state management library built for Vue. It is designed to consolidate shared data in a single place and ensure that all components read from the same source and update it according to the same rules.
Core concepts
Vuex has four fundamental building blocks:
- State: The single source of truth for the application. All components read from here.
- Mutation: The only way to change state. Must be synchronous.
- Action: Handles asynchronous operations; commits a mutation when done.
- Getter: Derived, computed values calculated from state.
A simple shopping cart example:
const store = new Vuex.Store({
state: {
cartItems: []
},
mutations: {
ADD_ITEM(state, item) {
state.cartItems.push(item);
},
REMOVE_ITEM(state, itemId) {
state.cartItems = state.cartItems.filter(i => i.id !== itemId);
}
},
actions: {
addToCart({ commit }, item) {
// An async check could go here, e.g. a stock availability query
commit('ADD_ITEM', item);
}
},
getters: {
cartTotal(state) {
return state.cartItems.reduce((sum, item) => sum + item.price, 0);
}
}
});
Accessing the store from a component
Once the store is injected into the Vue instance, every component can reach it via this.$store:
// Product component
methods: {
addProduct(product) {
this.$store.dispatch('addToCart', product);
}
},
computed: {
total() {
return this.$store.getters.cartTotal;
}
}
The cart component and the product list component can live in completely different parts of the tree — because both read from the same store, synchronization happens automatically.
mapState and mapGetters
Writing this.$store.state.cartItems every time gets tedious. Vuex’s helper functions cut that down:
import { mapState, mapGetters } from 'vuex';
computed: {
...mapState(['cartItems']),
...mapGetters(['cartTotal'])
}
This makes this.cartItems and this.cartTotal directly available as computed properties.
The difference between mutations and actions
When I first saw this split I thought it was unnecessary: why have both mutations and actions? The answer is all about asynchronous operations.
A mutation must always be synchronous. This rule is what allows Vuex DevTools to record state changes in the correct order. A non-synchronous mutation makes it nearly impossible for the tooling to track what changed and when.
Actions take on the async work. Make the API call, receive the result, then commit the mutation:
actions: {
async fetchCart({ commit }, userId) {
const response = await fetch(`/api/cart/${userId}`);
const items = await response.json();
commit('SET_CART', items);
}
}
This separation can feel overly strict at first, but you come to appreciate it over time. Because every state change goes through a mutation, you can see exactly why and when each change happened, right there in DevTools.
When does Vuex actually make sense?
Not every Vue project needs Vuex. For a short-lived form UI with just a few components, props and $emit are perfectly sufficient. Consider Vuex when:
- Multiple unrelated components need to read the same data.
- Data fetched from the server is used in many parts of the application.
- A single user action affects more than one component.
If two or three of those apply, setting up a store requires a bit of upfront work but pays off quickly.
One warning though: jumping in too early and pushing everything into the store is its own trap. Moving state that genuinely belongs to a single component — like a form’s isLoading flag — into the store adds unnecessary complexity. The rule is simple: if data concerns only one component, keep it local; if it concerns more than one, move it to the store.
Splitting the store with modules
As an application grows, a single store file starts to balloon. Vuex modules let you break it into pieces:
const cartModule = {
namespaced: true,
state: { items: [] },
mutations: { /* ... */ },
actions: { /* ... */ }
};
const store = new Vuex.Store({
modules: {
cart: cartModule,
user: userModule
}
});
With namespaced: true, access becomes this.$store.dispatch('cart/addToCart', item). Without that option, modules can silently overwrite each other’s mutation names — a rare but deeply annoying bug.
Inspecting with Vuex DevTools
The Vue DevTools browser extension becomes significantly more useful when paired with Vuex. You can see every mutation — what data it carried, exactly when it fired — and step through state history with time-travel debugging. Tracking down the source of odd UI behavior gets genuinely easier with this tool.
When I first started using Vuex it felt like over-engineering. Then I recalled the code where I was trying to pass a single cartItems array to five different components through a chain of $emit calls — and suddenly understood why the store exists.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.