Table of Contents
-
- 1.1 Core Features of Vue.js
- 1.2 Vue.js vs. Other Frameworks (React, Angular)
- 1.3 A Simple Vue.js Example
-
- 2.1 The Problem with Prop Drilling
- 2.2 When to Use a State Manager
-
- 3.1 What is Vuex?
- 3.2 Core Principles: Unidirectional Data Flow
- 3.3 Vuex vs. Other State Managers (Redux, MobX)
-
- 4.1 State: The Single Source of Truth
- 4.2 Getters: Derived State (Computed Properties for Store)
- 4.3 Mutations: Synchronous State Changes
- 4.4 Actions: Asynchronous Operations
- 4.5 Modules: Scaling with Modular Stores
-
Setting Up Vuex in a Vue.js App
- 5.1 Installation
- 5.2 Creating Your First Store
- 5.3 Injecting the Store into Vue
-
Practical Example: Building a Vuex-Powered App
- 6.1 Project Overview: A Shopping Cart
- 6.2 Defining State, Getters, Mutations, and Actions
- 6.3 Connecting Components to the Store
-
Advanced Vuex: Modules, Namespacing, and Optimization
- 7.1 Modules: Splitting the Store for Large Apps
- 7.2 Namespacing: Avoiding Naming Conflicts
- 7.3 Performance Optimization:
mapStateand Memoization
-
Vuex 4 vs. Pinia: The Future of State Management
- 8.1 What is Pinia?
- 8.2 Key Differences: Vuex vs. Pinia
- 8.3 Migrating from Vuex to Pinia
-
- 9.1 Keep State Normalized
- 9.2 Use Mutations for Synchronous Changes Only
- 9.3 Avoid Overusing Vuex for Local State
- 9.4 Leverage TypeScript for Type Safety
1. What is Vue.js?
Vue.js (often called “Vue”) is a progressive JavaScript framework for building user interfaces. Unlike monolithic frameworks (e.g., Angular), Vue is designed to be incrementally adoptable—you can use as much or as little of it as needed. Its core library focuses on the view layer, making it easy to integrate with other libraries or existing projects.
1.1 Core Features of Vue.js
- Declarative Rendering: Use HTML-based templates to declaratively render data to the DOM.
- Two-Way Data Binding: With
v-model, sync form input values with component state effortlessly. - Component-Based Architecture: Build reusable, self-contained components to compose complex UIs.
- Virtual DOM: Optimizes rendering by updating only changed DOM nodes (like React).
- Directives: Special attributes (e.g.,
v-if,v-for,v-bind) to add reactive behavior to DOM elements. - Composition API (Vue 3+): A flexible way to organize component logic, replacing the Options API for larger apps.
1.2 Vue.js vs. Other Frameworks
| Feature | Vue.js | React | Angular |
|---|---|---|---|
| Learning Curve | Gentle (HTML/CSS/JS familiarity) | Moderate (JSX, functional concepts) | Steep (TypeScript, RxJS, CLI) |
| Template Syntax | HTML-based templates | JSX (JavaScript in HTML) | HTML templates + TypeScript |
| State Management | Vuex/Pinia (official) | Redux/MobX/Zustand (community) | NgRx (RxJS-based, official) |
| Flexibility | Progressive (adopt piecemeal) | Unopinionated (choose your tools) | Opinionated (full-featured) |
1.3 A Simple Vue.js Example
Let’s start with a “Hello World” component to see Vue in action. Using the Options API (simpler for small apps):
<!-- HelloWorld.vue -->
<template>
<div>
<h1>{{ message }}</h1>
<input v-model="message" placeholder="Edit me">
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello, Vue!" // Reactive state
};
}
};
</script>
Here, {{ message }} interpolates the message data property into the template, and v-model creates two-way binding between the input and message.
2. Why State Management Matters
As Vue.js apps scale, components often need to share or modify the same data. For example:
- A user’s authentication status (shared across headers, dashboards, etc.).
- A shopping cart (updated by “Add to Cart” buttons and displayed in a cart icon).
2.1 The Problem with Prop Drilling
Without a state manager, you might pass data via props (parent → child) and emit events (child → parent). For deep component trees, this leads to prop drilling—passing props through intermediate components that don’t use the data themselves. This becomes unmaintainable:
Parent → Child A → Child B → Child C (needs the data)
2.2 When to Use a State Manager
Use a state manager like Vuex when:
- Multiple components need access to the same state.
- State is modified by multiple components.
- State changes are complex (e.g., async API calls, validation).
3. Introducing Vuex
Vuex is Vue’s official state management pattern + library. It centralizes application state in a single store, ensuring predictable state changes via strict rules.
3.1 What is Vuex?
Vuex is inspired by Flux and Redux, following these core principles:
- Single Source of Truth: The state of your app is stored in a single object tree within the store.
- State is Read-Only: You cannot directly modify state—only via mutations (synchronous) or actions (asynchronous).
- Changes are Made with Pure Functions: Mutations are pure functions that take the current state and a payload to modify it.
3.2 Core Principles: Unidirectional Data Flow
Vuex enforces a strict data flow:
- Components dispatch actions (e.g., fetch data from an API).
- Actions commit mutations (synchronous functions that update state).
- Mutations modify the state (single source of truth).
- Components reactively re-render when state changes.

Source: Vuex Official Docs
4. Vuex Core Concepts
Let’s break down Vuex’s core building blocks with code examples.
4.1 State: The Single Source of Truth
The state is a plain object holding your app’s shared data. Think of it as a “global data store.”
// store/index.js
import { createStore } from 'vuex';
const store = createStore({
state() {
return {
count: 0, // Shared state: a simple counter
user: null // Shared state: current user
};
}
});
export default store;
To access state in a component, use this.$store.state (Options API) or useStore (Composition API):
// Composition API (Vue 3)
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
console.log(store.state.count); // 0
}
};
4.2 Getters: Derived State
Getters are like computed properties for the store—they return derived state based on the current state. Use them to avoid redundant calculations in components.
// store/index.js
const store = createStore({
state() {
return {
todos: [
{ id: 1, text: 'Learn Vuex', done: true },
{ id: 2, text: 'Build an app', done: false }
]
};
},
getters: {
// Get completed todos
completedTodos(state) {
return state.todos.filter(todo => todo.done);
},
// Get count of completed todos
completedTodosCount(state, getters) {
return getters.completedTodos.length; // Reuse other getters
}
}
});
Access getters in components with this.$store.getters:
// Component
setup() {
const store = useStore();
console.log(store.getters.completedTodosCount); // 1
}
4.3 Mutations: Synchronous State Changes
Mutations are the only way to modify state. They are synchronous functions that take state and a payload (optional data to update state).
Rule: Always use mutations to change state—never modify state directly (e.g., this.$store.state.count = 1 is forbidden!).
// store/index.js
const store = createStore({
state() {
return { count: 0 };
},
mutations: {
// Mutation to increment count
increment(state, payload) {
state.count += payload || 1; // Default payload: 1
},
// Mutation to decrement count
decrement(state) {
state.count--;
}
}
});
Commit mutations in components with store.commit:
// Component
setup() {
const store = useStore();
store.commit('increment', 5); // count becomes 5
}
4.4 Actions: Asynchronous Operations
Actions handle asynchronous logic (e.g., API calls) and then commit mutations. They can return promises for chaining.
// store/index.js
const store = createStore({
state() {
return { user: null };
},
mutations: {
setUser(state, user) {
state.user = user; // Update state with fetched user
}
},
actions: {
// Async action to fetch user from API
async fetchUser({ commit }, userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
commit('setUser', user); // Commit mutation after fetch
} catch (error) {
console.error('Failed to fetch user:', error);
}
}
}
});
Dispatch actions in components with store.dispatch:
// Component
setup() {
const store = useStore();
store.dispatch('fetchUser', 123); // Fetch user with ID 123
}
4.5 Modules: Scaling with Modular Stores
For large apps, the store can become bloated. Modules split the store into smaller, reusable pieces (e.g., user, cart, todos).
// store/modules/cart.js
export default {
state() {
return { items: [] };
},
mutations: {
addItem(state, item) {
state.items.push(item);
}
},
actions: {
addToCart({ commit }, product) {
commit('addItem', product);
}
}
};
// store/index.js
import { createStore } from 'vuex';
import cart from './modules/cart';
import user from './modules/user';
const store = createStore({
modules: {
cart, // Register cart module
user // Register user module
}
});
Access module state with store.state.cart.items (nested under the module name).
5. Setting Up Vuex in a Vue.js App
Let’s walk through installing Vuex and integrating it into a Vue 3 app.
5.1 Installation
First, install Vuex via npm or yarn:
npm install vuex@next --save # Vuex 4 for Vue 3
# or
yarn add vuex@next --save
5.2 Creating Your First Store
Create a store directory in your project root, with an index.js file:
// src/store/index.js
import { createStore } from 'vuex';
export default createStore({
state() {
return { count: 0 };
},
mutations: {
increment(state) { state.count++; }
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit('increment'), 1000);
}
}
});
5.3 Injecting the Store into Vue
In your app’s entry file (e.g., main.js), import the store and pass it to createApp:
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import store from './store'; // Import the store
createApp(App)
.use(store) // Inject store into the app
.mount('#app');
Now the store is available to all components via this.$store (Options API) or useStore (Composition API).
6. Practical Example: Building a Vuex-Powered App
Let’s build a simple “Shopping Cart” app to tie together Vuex concepts.
6.1 Project Overview
Features:
- Add products to the cart.
- Remove products from the cart.
- Calculate total price.
6.2 Defining State, Getters, Mutations, and Actions
// store/modules/cart.js
export default {
state() {
return { items: [] }; // Cart items: { id, name, price, quantity }
},
getters: {
cartTotal(state) {
return state.items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
}
},
mutations: {
addItem(state, product) {
const existingItem = state.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity++;
} else {
state.items.push({ ...product, quantity: 1 });
}
},
removeItem(state, productId) {
state.items = state.items.filter(item => item.id !== productId);
}
},
actions: {
addToCart({ commit }, product) {
commit('addItem', product); // Commit mutation to add product
},
removeFromCart({ commit }, productId) {
commit('removeItem', productId); // Commit mutation to remove product
}
}
};
6.3 Connecting Components to the Store
In a product list component, dispatch the addToCart action when a button is clicked:
<!-- ProductList.vue -->
<template>
<div class="products">
<div v-for="product in products" :key="product.id">
<h3>{{ product.name }}</h3>
<p>Price: ${{ product.price }}</p>
<button @click="addToCart(product)">Add to Cart</button>
</div>
</div>
</template>
<script>
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 25 }
];
const addToCart = (product) => {
store.dispatch('addToCart', product); // Dispatch action
};
return { products, addToCart };
}
};
</script>
In a cart component, display the cart items and total using state and getters:
<!-- Cart.vue -->
<template>
<div class="cart">
<h2>Your Cart ({{ items.length }})</h2>
<div v-for="item in items" :key="item.id">
<p>{{ item.name }} x{{ item.quantity }} - ${{ item.price * item.quantity }}</p>
<button @click="removeFromCart(item.id)">Remove</button>
</div>
<p><strong>Total: ${{ cartTotal }}</strong></p>
</div>
</template>
<script>
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
return {
items: store.state.cart.items, // Access module state
cartTotal: store.getters.cartTotal, // Access getter
removeFromCart: (id) => store.dispatch('removeFromCart', id) // Dispatch action
};
}
};
</script>
7. Advanced Vuex: Modules, Namespacing, and Optimization
7.1 Modules: Splitting the Store
As apps grow, split the store into modules (e.g., cart, user, todos). Each module can have its own state, getters, mutations, and actions.
7.2 Namespacing: Avoiding Naming Conflicts
By default, module mutations/actions are global—this can cause conflicts (e.g., two modules with an increment mutation). Enable namespacing to scope them to the module:
// store/modules/cart.js
export default {
namespaced: true, // Enable namespacing
mutations: {
increment(state) { ... } // Now: cart/increment
}
};
Access namespaced mutations/actions with moduleName/mutationName:
// Component
store.commit('cart/increment'); // Namespaced commit
store.dispatch('user/fetchUser'); // Namespaced dispatch
7.3 Performance Optimization
mapState/mapGettersHelpers: Simplify accessing state/getters in components:import { mapState, mapGetters } from 'vuex'; export default { computed: { ...mapState('cart', ['items']), // Map cart.items to component computed prop ...mapGetters('cart', ['cartTotal']) // Map cart.cartTotal } };- Memoization: Use
createSelector(fromvuex) to memoize expensive getters and avoid redundant recalculations.
8. Vuex 4 vs. Pinia: The Future of State Management
Vuex has long been the official state manager, but Vue 3 introduced Pinia—a lighter, simpler alternative. In 2021, the Vue team announced Pinia as the new recommended state manager (Vuex 5 development was halted).
8.1 What is Pinia?
Pinia is a state management library for Vue.js with:
- A simpler API (no mutations—directly modify state in actions).
- Better TypeScript support (no need for
thiscontext hacks). - No nested modules (flat structure with stores).
- Smaller bundle size (~1KB vs. Vuex’s ~20KB).
8.2 Key Differences: Vuex vs. Pinia
| Feature | Vuex 4 | Pinia |
|---|---|---|
| Mutations | Required (synchronous state changes) | No mutations (actions modify state directly) |
| TypeScript Support | Limited (requires decorators) | Native (type-safe by design) |
| Modules | Nested modules with namespacing | Flat stores (each store is a module) |
| Bundle Size | ~20KB | ~1KB |
8.3 Migrating from Vuex to Pinia
Migrating is straightforward. A Pinia store replaces a Vuex module:
// Pinia Store (stores/cart.js)
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: {
cartTotal: (state) => state.items.reduce((t, i) => t + i.price * i.quantity, 0)
},
actions: {
addItem(product) { // No mutation needed—directly modify state!
const existing = state.items.find(i => i.id === product.id);
existing ? existing.quantity++ : state.items.push({ ...product, quantity: 1 });
}
}
});
Use the store in components:
// Component
import { useCartStore } from '@/stores/cart';
setup() {
const cartStore = useCartStore();
cartStore.addItem(product); // Directly call action
}
9. Best Practices for Vuex
- Normalize State: Store arrays as objects with IDs (e.g.,
{ 1: { id:1, ... }, 2: { id:2, ... } }) for faster lookups. - Use Mutations for Sync Only: Never put async logic in mutations—use actions instead.
- Avoid Overusing Vuex: Keep local state (e.g., form inputs) in components, not Vuex.
- Namespaced Modules: Always use
namespaced: truefor modules to avoid conflicts. - TypeScript: Use TypeScript to enforce type safety for state, getters, and actions.
10. Common Pitfalls and How to Avoid Them
- Mutating State Directly: Always use mutations/actions—direct changes bypass Vuex’s reactivity and debugging tools.
- Async in Mutations: Mutations must be synchronous. Use actions for API calls.
- Deeply Nested State: Avoid nested state (e.g.,
user.addresses[0].street). Normalize or usevuex-map-fieldsfor flattening. - Overfetching State: Use
mapStateorcomputedto extract only needed state properties (avoids unnecessary re-renders).
11. Conclusion
Vue.js’s simplicity makes it ideal for building everything from small apps to enterprise-level systems. As your app grows, Vuex (or Pinia) becomes indispensable for managing shared state, ensuring predictability, and simplifying component communication.
While Vuex has been the go-to for years, Pinia is now the recommended state manager for Vue 3+ apps, thanks to its simpler API and better TypeScript support. Regardless of which you choose, mastering state management is key to building scalable Vue.js applications.