Vue.js 14 min read

Vue.js State Management 2026: Pinia vs Vuex (Pinia Wins) 🚀

Complete guide to Vue 3 state management with Pinia (official replacement for Vuex), modular stores, TypeScript-first patterns, plugins, SSR hydration, persistence, and production scaling strategies.

#vuejs #pinia #vuex #vue 3 state management #composables
Guide Vue.js

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

FeaturePinia (Vue 3+)Vuex (Legacy)
ModularityMultiple stores❌ Single store + modules
TypeScriptNative❌ Manual typing
MutationsDirect state✅ Required
DevToolsPinia Inspector✅ Vuex tab
SSRNative hydration❌ Complex
Bundle Size1.1KB4.2KB
Learning CurveComposition APIOptions 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 PatternPinia Equivalent
this.$store.stateuseStore()
mutationsDirect state
actionsDirect async
getterscomputed()
ModulesMultiple stores
mapStatestoreToRefs()

📊 Production Metrics

MetricVuexPinia
Bundle Size14KB1.1KB
TypeScriptManualNative
DevToolsVuex tabPinia Inspector
SSR SupportComplexNative
Learning CurveHighLow

🎯 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

Chat with us