When a Vue SPA grows, the application state becomes dozens of interdependent variables. Mutations are scattered across components, and tracking who changed what is impossible without a log. For example, in a project with 50 components and 200 state variables, any change can trigger a cascade of bugs. We configure Vuex — the standard state management for Vue — to enforce a strict unidirectional data flow, making every mutation traceable. It works for both Vue 2 and Vue 3 (version 4). While alternatives like Pinia exist, Vuex remains the de facto standard for Vue 2 and legacy codebases. Setting it up involves several steps, and you get a predictable state with a full change history. Full typing and modular architecture are key to scaling.
Problems We Solve
- Broken unidirectional flow. Direct state mutations from components are a common mistake leading to unpredictable behavior. Vuex logs every mutation, and with DevTools you can instantly find the source.
- Lack of modularity. All state in a single file (over 1000 lines) is hard to maintain. We split into modules by business domain (cart, auth, ui), each with its own context.
- Debugging without DevTools. Without a timeline of mutations, it's impossible to understand what caused a bug. Vue DevTools show mutation history and state at any point, reducing error search time by 2–3 times.
- Missing TypeScript. JavaScript-only stores are prone to typos in getter and action names. We implement a typed store wrapper, giving autocompletion and eliminating up to 60% of runtime errors. The setup cost pays off through reduced debugging time.
How We Do It
We use Vuex 4 for Vue 3 or vuex@3 for Vue 2. Stack: TypeScript, modules with namespaced: true, vuex-persistedstate for token and theme persistence, Jest for testing. Each module is a typed class with state interface, getters, mutations, and actions.
Vuex vs Pinia (for context)
| Criteria | Vuex | Pinia |
|---|---|---|
| Typing | Wrapper required | Built-in |
| DevTools | Full support | Full support |
| Mutations | Required | Not needed |
| Compatibility | Vue 2 and Vue 3 | Vue 3 only |
| Size | ~10 KB | ~2 KB |
Vuex remains relevant for large Vue 2 projects and migrations. According to our experience, it reduces debugging time by 2–3 times thanks to strict rules and DevTools. Support budget savings can reach 40%.
What You Get After Setup
| Component | Result |
|---|---|
| Store architecture | Modular structure with 3–7 clearly separated modules |
| Typing | Full TypeScript with IDE autocompletion |
| Persisted state | Token and theme saved to localStorage |
| Tests | Key modules covered with Jest unit tests |
| Documentation | README with module descriptions and usage examples |
How to Structure Vuex Modules Properly
Each module is an isolated context. Example cart module:
// store/modules/cart.ts import type { Module } from 'vuex' import type { RootState } from '../types' interface CartState { items: CartItem[] loading: boolean } export const cart: Module<CartState, RootState> = { namespaced: true, state: () => ({ items: [], loading: false, }), getters: { total: (state) => state.items.reduce((sum, i) => sum + i.price * i.quantity, 0), }, mutations: { ADD_ITEM(state, product: Product) { const existing = state.items.find(i => i.id === product.id) if (existing) { existing.quantity++ } else { state.items.push({ ...product, quantity: 1 }) } }, CLEAR_CART(state) { state.items = [] }, }, actions: { async checkout({ commit, state }) { commit('SET_LOADING', true) await api.post('/orders', { items: state.items }) commit('CLEAR_CART') commit('SET_LOADING', false) }, }, } Why Use a Typed Store Wrapper?
For full typing, we create a useStore wrapper:
// store/typed-store.ts import { useStore as baseUseStore, Store } from 'vuex' import type { InjectionKey } from 'vue' import type { RootState } from './types' export const key: InjectionKey<Store<RootState>> = Symbol() export function useStore(): Store<RootState> { return baseUseStore(key) } Register it in main.ts and use in components:
import { useStore } from '@/store/typed-store' const store = useStore() // fully typed With a typed store wrapper, autocompletion works in every component, and type errors are caught at compile time.
How to Test Vuex Modules?
We verify key scenarios: adding products, increasing quantity, checking out.
import { createStore } from 'vuex' import { cart } from '@/store/modules/cart' test('adding a product increments the count', () => { const store = createStore({ modules: { cart } }) store.dispatch('cart/addItem', { id: '1', price: 100 }) expect(store.getters['cart/count']).toBe(1) }) This approach ensures store changes don't break business logic.
Real-world case
On one project (an e-commerce site on Vue 2), the cart state was scattered across 12 components. After implementing a cart module with persisted state, debugging time dropped by a factor of 3, and the codebase shrank by 15%.Work Process
- Analysis — review current code, identify pain points.
- Design — define modules, types, interfaces.
- Implementation — write store, types, persisted state.
- Integration — connect to components, replace local state.
- Testing — unit tests for key modules and integration.
- Deployment — verify in production, fix bugs.
Timeline: typically 2–5 days depending on legacy code volume. Cost is calculated individually. We’ve been doing Vue development for over 5 years and have completed 30+ Vuex projects, so we know all the pitfalls. Get a consultation on setting up Vuex for your project — we'll evaluate your code and propose the optimal architecture. Order a turnkey setup — we guarantee a transparent change history and easy maintenance.
Common Mistakes in Vuex Setup
- Not setting
namespaced: true— modules will conflict. - Not typing mutations — use constants or TypeScript enums.
- Storing computed data in state — use getters instead.
- Calling mutations directly from components — always go through actions.
These simple rules eliminate up to 70% of bugs from the start.







