Pinia is the official state management library for Vue 3, replacing Vuex with a simpler and more TypeScript-friendly API.
Why Pinia?
- Simple API - No mutations, just actions
- TypeScript support - Excellent type inference
- DevTools integration - Time-travel debugging
- Lightweight - Small bundle size
- Modular - Stores are independent by default
Installation
bash
npm install piniaSetup
javascript
// 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 a Store
Setup Store (Recommended)
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const user = ref(null)
const token = ref(null)
// Getters
const isAuthenticated = computed(() => !!token.value)
const userName = computed(() => user.value?.name || 'Guest')
// Actions
function setUser(userData, userToken) {
user.value = userData
token.value = userToken
}
function logout() {
user.value = null
token.value = null
}
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
const data = await response.json()
setUser(data.user, data.token)
}
return {
user,
token,
isAuthenticated,
userName,
setUser,
logout,
fetchUser
}
})Options Store
javascript
// stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price, 0)
},
actions: {
addItem(item) {
this.items.push(item)
this.calculateTotal()
},
removeItem(index) {
this.items.splice(index, 1)
this.calculateTotal()
},
calculateTotal() {
this.total = this.items.reduce((sum, item) => sum + item.price, 0)
}
}
})Using Stores in Components
vue
<template>
<div>
<div v-if="user">
<p>Welcome, {{ userName }}!</p>
<button @click="logout">Logout</button>
</div>
<div v-else>
<button @click="login">Login</button>
</div>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { user, userName } = storeToRefs(userStore)
async function login() {
await userStore.fetchUser(1)
}
function logout() {
userStore.logout()
}
</script>Store Composition
You can use stores inside other stores:
javascript
// stores/order.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'
export const useOrderStore = defineStore('order', () => {
const userStore = useUserStore()
const cartStore = useCartStore()
const orders = ref([])
async function createOrder() {
if (!userStore.isAuthenticated) {
throw new Error('User must be logged in')
}
const order = {
user: userStore.user,
items: cartStore.items,
total: cartStore.totalPrice
}
const response = await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(order)
})
const data = await response.json()
orders.value.push(data.order)
cartStore.clear()
return data.order
}
return {
orders,
createOrder
}
})Actions with Async/Await
javascript
// stores/product.js
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', () => {
const products = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchProducts() {
loading.value = true
error.value = null
try {
const response = await fetch('/api/products')
const data = await response.json()
products.value = data.products
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
products,
loading,
error,
fetchProducts
}
})Store Plugins
javascript
// plugins/pinia-persist.js
import { watch } from 'vue'
export function createPersistedState(options = {}) {
return ({ store }) => {
const key = options.key ?? `pinia-${store.$id}`
const storage = options.storage ?? localStorage
// Restore state
const savedState = storage.getItem(key)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// Save state
store.$subscribe((mutation, state) => {
storage.setItem(key, JSON.stringify(state))
})
}
}
// Usage in main.js
import { createPinia } from 'pinia'
import { createPersistedState } from './plugins/pinia-persist'
const pinia = createPinia()
pinia.use(createPersistedState())Testing Stores
javascript
// tests/stores/user.spec.js
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
describe('User Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('sets user correctly', () => {
const store = useUserStore()
const user = { name: 'John', email: 'john@example.com' }
store.setUser(user, 'token123')
expect(store.user).toEqual(user)
expect(store.token).toBe('token123')
expect(store.isAuthenticated).toBe(true)
})
it('logs out correctly', () => {
const store = useUserStore()
store.setUser({ name: 'John' }, 'token')
store.logout()
expect(store.user).toBeNull()
expect(store.token).toBeNull()
expect(store.isAuthenticated).toBe(false)
})
})Best Practices
- 1Use setup stores for better TypeScript inference
- 2Keep stores focused - one store per domain
- 3Use composables for reusable logic across stores
- 4Avoid nested state - keep state flat
- 5Use storeToRefs when destructuring in components
Pinia provides a simple and powerful state management solution for Vue 3 applications with excellent TypeScript support and developer experience.