Vue State Management with Pinia
Pinia is the official state management library for Vue. It provides a simple, type-safe API and works seamlessly with the Composition API and Vue DevTools.
Setting Up Pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')Defining a Store
Stores in Pinia are defined using the defineStore() function. There are two syntaxes: Option Store (similar to Vue Options API) and Setup Store (similar to Composition API).
// stores/counter.js - Option Store syntax
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter',
}),
getters: {
doubleCount: (state) => state.count * 2,
// Accessing other getters
isEven() {
return this.count % 2 === 0
},
// Getter with parameters
countPlusN: (state) => (n) => state.count + n,
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 1000))
this.count++
},
reset() {
this.count = 0
},
},
})
// stores/todos.js - Setup Store syntax (recommended)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useTodoStore = defineStore('todos', () => {
// State
const todos = ref([])
const filter = ref('all')
const loading = ref(false)
// Getters
const completedTodos = computed(() =>
todos.value.filter((todo) => todo.completed)
)
const activeTodos = computed(() =>
todos.value.filter((todo) => !todo.completed)
)
const filteredTodos = computed(() => {
switch (filter.value) {
case 'completed':
return completedTodos.value
case 'active':
return activeTodos.value
default:
return todos.value
}
})
// Actions
async function fetchTodos() {
loading.value = true
try {
const response = await fetch('/api/todos')
todos.value = await response.json()
} finally {
loading.value = false
}
}
function addTodo(text) {
todos.value.push({
id: Date.now(),
text,
completed: false,
})
}
function toggleTodo(id) {
const todo = todos.value.find((t) => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function deleteTodo(id) {
const index = todos.value.findIndex((t) => t.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
function setFilter(newFilter) {
filter.value = newFilter
}
return {
// State
todos,
filter,
loading,
// Getters
completedTodos,
activeTodos,
filteredTodos,
// Actions
fetchTodos,
addTodo,
toggleTodo,
deleteTodo,
setFilter,
}
})Using Stores in Components
<script setup>
import { useTodoStore } from '@/stores/todos'
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const todoStore = useTodoStore()
const counterStore = useCounterStore()
// Direct access to state and getters
console.log(todoStore.todos)
console.log(todoStore.filteredTodos)
// Call actions
todoStore.addTodo('Learn Pinia')
todoStore.toggleTodo(1)
// Destructuring (loses reactivity)
const { todos, filter } = todoStore // ❌ Not reactive
// Use storeToRefs to maintain reactivity
const { todos, filter, completedTodos } = storeToRefs(todoStore) // ✅ Reactive
const { addTodo, toggleTodo } = todoStore // Actions don't need storeToRefs
// Subscribe to state changes
todoStore.$subscribe((mutation, state) => {
console.log('Store mutated:', mutation.type)
console.log('New state:', state)
// Persist to localStorage
localStorage.setItem('todos', JSON.stringify(state.todos))
})
// Watch specific state
watch(
() => todoStore.todos.length,
(newLength) => {
console.log(`Todos count: ${newLength}`)
}
)
</script>
<template>
<div>
<h1>Todos ({{ todos.length }})</h1>
<div>
<button @click="() => setFilter('all')">All</button>
<button @click="() => setFilter('active')">Active</button>
<button @click="() => setFilter('completed')">Completed</button>
</div>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="() => toggleTodo(todo.id)"
/>
{{ todo.text }}
</li>
</ul>
</div>
</template>Store Composition
Stores can use other stores, enabling modular and composable state management.
// stores/auth.js
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref(null)
const isAuthenticated = computed(() => !!user.value)
async function login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
})
const data = await response.json()
user.value = data.user
token.value = data.token
}
function logout() {
user.value = null
token.value = null
}
return { user, token, isAuthenticated, login, logout }
})
// stores/cart.js - uses auth store
export const useCartStore = defineStore('cart', () => {
const authStore = useAuthStore() // ✅ Use another store
const items = ref([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
async function addItem(product) {
if (!authStore.isAuthenticated) {
throw new Error('Please login to add items')
}
items.value.push({
id: product.id,
name: product.name,
price: product.price,
quantity: 1,
})
// Sync with backend
await fetch('/api/cart', {
method: 'POST',
headers: {
Authorization: `Bearer ${authStore.token}`,
},
body: JSON.stringify({ productId: product.id }),
})
}
return { items, total, addItem }
})Pinia Plugins
// Persistence plugin
import { createPinia } from 'pinia'
function persistencePlugin({ store }) {
// Load persisted state
const stored = localStorage.getItem(store.$id)
if (stored) {
store.$patch(JSON.parse(stored))
}
// Persist on change
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
const pinia = createPinia()
pinia.use(persistencePlugin)Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free