Vue 3 Pinia State Management: The Complete Guide
Vue 3 Pinia State Management: The Complete Guide
Pinia has become the official state management library for Vue.js, offering a more intuitive and type-safe alternative to Vuex. Let's explore how to use it effectively in your Vue 3 applications.
Why Pinia Over Vuex?
Pinia offers several advantages:
- No mutations - Direct state modification is cleaner
- Better TypeScript support - Full type inference out of the box
- No modules - Each store is independent
- Smaller bundle size - Only ~1KB gzipped
- DevTools support - Full Vue DevTools integration
Installation and Setup
# npm
npm install pinia
# yarn
yarn add pinia
# pnpm
pnpm add pinia
Creating the Pinia Instance
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
Defining Stores
Pinia stores can be defined using Setup Stores or Option Stores. Let's explore both approaches.
Option Stores
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// State
state: () => ({
count: 0,
name: 'My Counter',
}),
// Getters
getters: {
doubleCount: (state) => state.count * 2,
// Using other getters
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
// Actions
actions: {
increment() {
this.count++
},
async fetchCount() {
const response = await fetch('/api/count')
const data = await response.json()
this.count = data.count
},
},
})
Setup Stores (Recommended)
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const user = ref(null)
const loading = ref(false)
const error = ref(null)
// Getters
const isLoggedIn = computed(() => !!user.value)
const userName = computed(() => user.value?.name ?? 'Guest')
// Actions
async function login(email, password) {
loading.value = true
error.value = null
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
user.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
function logout() {
user.value = null
}
return { user, loading, error, isLoggedIn, userName, login, logout }
})
Using Stores in Components
Basic Usage
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
Destructuring with storeToRefs
Always use storeToRefs when destructuring state or getters to maintain reactivity:
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// Reactive refs for state and getters
const { count, doubleCount } = storeToRefs(store)
// Actions can be destructured directly
const { increment } = store
</script>
Direct State Modification
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// Direct modification (no mutations needed!)
store.count = 100
// Multiple changes at once
store.$patch({
count: 50,
name: 'Updated Counter',
})
// $patch with a function
store.$patch((state) => {
state.count++
state.name = state.name.toUpperCase()
})
</script>
Composing Stores
Pinia stores can use other stores, enabling powerful composition patterns.
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
const items = ref([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
)
function addItem(product) {
if (!userStore.isLoggedIn) {
throw new Error('Please login to add items to cart')
}
items.value.push(product)
}
return { items, total, addItem }
})
Using with Composition API
Pinia integrates seamlessly with Vue's Composition API:
<template>
<div v-if="loading">Loading...</div>
<div v-else>
<p>Welcome, {{ userName }}!</p>
<button @click="handleLogout">Logout</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userName, loading } = storeToRefs(userStore)
const { logout } = userStore
function handleLogout() {
logout()
// Redirect to login page
}
</script>
Plugins
Pinia supports plugins to extend functionality.
Persistence Plugin
// plugins/persist.js
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(({ store }) => {
// Load from localStorage
const stored = localStorage.getItem(store.$id)
if (stored) {
store.$patch(JSON.parse(stored))
}
// Watch for changes
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
})
export default pinia
Using the Plugin
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import pinia from './plugins/persist'
createApp(App).use(pinia).mount('#app')
Resetting State
Each store comes with a $reset method:
const store = useCounterStore()
// Reset to initial state
store.$reset()
Subscribing to Changes
State Subscriptions
const store = useCounterStore()
// Subscribe to state changes
store.$subscribe((mutation, state) => {
console.log('Type:', mutation.type)
console.log('Store ID:', mutation.storeId)
console.log('New State:', state)
})
Action Subscriptions
const store = useCounterStore()
// Subscribe to actions
store.$onAction(({ name, args, after, onError }) => {
console.log(`Action ${name} called with:`, args)
after((result) => {
console.log(`Action ${name} finished with:`, result)
})
onError((error) => {
console.error(`Action ${name} failed:`, error)
})
})
Best Practices
1. One Store Per Domain
// Good: Domain-based stores
stores/
├── user.js
├── cart.js
└── products.js
// Avoid: Single giant store
stores/
└── index.js // Too much in one file
2. Use Composables for Complex Logic
// composables/useAuth.js
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
export function useAuth() {
const userStore = useUserStore()
const router = useRouter()
async function login(credentials) {
await userStore.login(credentials)
router.push('/dashboard')
}
return { login }
}
3. Type Your Stores Properly
// stores/user.ts
import { defineStore } from 'pinia'
interface User {
id: number
name: string
email: string
}
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
}),
getters: {
isLoggedIn: (state): boolean => state.user !== null,
},
})
Conclusion
Pinia provides a clean, intuitive approach to state management in Vue 3 applications. Its Composition API-style syntax, excellent TypeScript support, and plugin ecosystem make it the ideal choice for modern Vue applications.
Start migrating your Vuex stores to Pinia today and enjoy a more pleasant development experience!