Vue.js State Management 2026: Pinia vs Vuex (Pinia Wins) 🚀
Pinia is the official Vue 3 state management library (Vuex 5 successor) with modular stores, TypeScript-first APIs, zero mutations boilerplate, DevTools integration, SSR hydration, and plugins ecosystem that powers 85% of production Vue apps [web:195]. Vuex remains viable for Vue 2 legacy but lacks Composition API integration and modularity [web:192].
🎯 Pinia vs Vuex Decision Matrix
| Feature | Pinia (Vue 3+) | Vuex (Legacy) |
|---|---|---|
| Modularity | ✅ Multiple stores | ❌ Single store + modules |
| TypeScript | ✅ Native | ❌ Manual typing |
| Mutations | ❌ Direct state | ✅ Required |
| DevTools | ✅ Pinia Inspector | ✅ Vuex tab |
| SSR | ✅ Native hydration | ❌ Complex |
| Bundle Size | 1.1KB | 4.2KB |
| Learning Curve | Composition API | Options API |
[image:202]
🏗️ Pinia Production Setup (5 Minutes)
Step 1: Install Pinia
Vue 3 + Pinia npm install pinia @vue/server-renderer npm install -D @pinia/nuxt # Nuxt integration
Step 2: Create Pinia Plugin
// plugins/pinia.client.ts (Nuxt 3)
import { createPinia } from 'pinia'
export default defineNuxtPlugin((nuxtApp) => {
const pinia = createPinia()
nuxtApp.vueApp.use(pinia)
// Production persistence
if (process.client) {
pinia.use(createPiniaPersistence({
storage: persistedState.localStorage()
}))
}
})
Step 3: Modular Store Pattern
// stores/auth.ts - TypeScript-first
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types/user'
export const useAuthStore = defineStore('auth', () => {
// State
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const loading = ref(false)
// Computed (derived state)
const isAuthenticated = computed(() => !!user.value && !!token.value)
const userName = computed(() => user.value?.name ?? 'Guest')
// Actions (async allowed!)
const login = async (credentials: LoginCredentials) => {
loading.value = true
try {
const { data } = await $fetch<{ user: User; token: string }>('/api/auth/login', {
method: 'POST',
body: credentials
})
user.value = data.user
token.value = data.token
} catch (error) {
throw createError({
statusCode: 401,
message: 'Invalid credentials'
})
} finally {
loading.value = false
}
}
const logout = () => {
user.value = null
token.value = null
navigateTo('/login')
}
return {
user,
token,
loading,
isAuthenticated,
userName,
login,
logout
}
})
🎯 Production Store Patterns
1. Multiple Modular Stores
// stores/todo.ts
export const useTodoStore = defineStore('todo', () => {
const todos = ref<Todo[]>([])
const loading = ref(false)
const fetchTodos = async () => {
loading.value = true
todos.value = await $fetch('/api/todos')
loading.value = false
}
const addTodo = async (todo: Partial<Todo>) => {
const newTodo = await $fetch('/api/todos', {
method: 'POST',
body: todo
})
todos.value.push(newTodo)
}
return { todos, loading, fetchTodos, addTodo }
})
// Usage in component
const authStore = useAuthStore()
const todoStore = useTodoStore()
2. StoreToRefs (Reactivity)
<script setup lang="ts">
const authStore = useAuthStore()
const { user, isAuthenticated } = storeToRefs(authStore)
// Fully reactive!
watch(isAuthenticated, (value) => {
if (!value) navigateTo('/login')
})
</script>
3. Plugins (Cross-Cutting Concerns)
// plugins/pinia-plugin-persisted.ts
import { PiniaPluginContext } from 'pinia'
export const piniaPluginPersisted = (context: PiniaPluginContext) => {
// Auto-persist auth stores
if (context.store.$id.includes('auth')) {
const persisted = localStorage.getItem(context.store.$id)
if (persisted) {
context.store.$patch(JSON.parse(persisted))
}
context.store.$subscribe(() => {
localStorage.setItem(context.store.$id, JSON.stringify(context.store.$state))
})
}
}
// Install
pinia.use(piniaPluginPersisted)
🛠️ SSR Hydration (Nuxt 3)
// stores/auth.server.ts - Server-side store
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
// Server-side data fetching
const nuxtApp = useNuxtApp()
if (process.server) {
// Hydrate from cookies/session
const session = await nuxtApp.$fetch('/api/auth/session')
user.value = session.user
}
return { user }
}, {
// SSR persistence
persist: {
storage: persistedState.cookiesWithOptions({
maxAge: 60 * 60 * 24 * 7 // 7 days
})
}
})
🎯 Vuex → Pinia Migration Guide
| Vuex Pattern | Pinia Equivalent |
|---|---|
this.$store.state | useStore() |
mutations | ❌ Direct state |
actions | ✅ Direct async |
getters | ✅ computed() |
| Modules | ✅ Multiple stores |
mapState | storeToRefs() |
📊 Production Metrics
| Metric | Vuex | Pinia |
|---|---|---|
| Bundle Size | 14KB | 1.1KB |
| TypeScript | Manual | Native |
| DevTools | Vuex tab | Pinia Inspector |
| SSR Support | Complex | Native |
| Learning Curve | High | Low |
🎯 Complete App Integration
<!-- components/TodoApp.vue -->
<script setup lang="ts">
const authStore = useAuthStore()
const todoStore = useTodoStore()
const { todos, loading } = storeToRefs(todoStore)
await todoStore.fetchTodos()
watch(authStore.isAuthenticated, (value) => {
if (!value) navigateTo('/login')
})
</script>
<template>
<div v-if="loading">Loading...</div>
<TodoList v-else :todos="todos" />
</template>
🚀 Nuxt 3 + Pinia (Production)
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt', '@pinia-plugin-persisted/nuxt'],
pinia: {
storesDirs: ['./stores/**']
}
})
🎯 Production Checklist
✅ [] Pinia (not Vuex) for Vue 3 ✅ [] TypeScript-first stores ✅ [] storeToRefs() for reactivity ✅ [] Multiple modular stores ✅ [] SSR hydration (Nuxt) ✅ [] Persistence plugin ✅ [] Pinia DevTools Inspector ✅ [] Auto-import stores
🎯 Final Thoughts
Pinia = Vuex 5 (official successor). Modular stores, TypeScript-first, zero mutations, SSR-native, and 1.1KB bundle make Pinia the clear choice for Vue 3 production apps [web:195].
2026 Vue State Strategy: Composables → Component state (70%) Pinia → Global state (25%) Server-side → Initial data (5%)
Vuex legacy = 2022. Pinia production = 2026. Build scalable Vue apps with modern state management 🚀.
Pinia Docs: pinia.vuejs.org | Vue 3 State: vuejs.org/guide/scaling-up/state.html