Pinia: A Modern, Simpler State Manager for Vue 3
Download and install Pinia first:
npm install pinia --save
In Vue 3's main.js, initialize Pinia:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import RootApp from './RootApp.vue'
const storeInstance = createPinia()
const vueApp = createApp(RootApp)
vueApp.use(storeInstance)
vueApp.mount('#app')
Pinia replaces Vuex for Vue 3 primarily due to these advantages:
- No redundant mutations; devtools integration works without them.
- TypeScript support is built-in with reliable type inference.
- Flat store architecture eliminates nested modules, while allowing implicit store composition via imports.
- Dynamic store registration is automatic, eliminating manual setup.
- API follows Vue 3's Composable API style, using direct function calls and auto-completion.
State Management
Direct State Modification and Batch Updates
Pinia allows direct state mutation:
const catalogStore = useProductCatalog()
catalogStore.productCount++
For batch updates, use $patch with an object or function:
// Object patch
catalogStore.$patch({ productCount: 10, isUpdated: true })
// Function patch
catalogStore.$patch(state => {
state.products.push({ name: 'Wireless Headphones', price: 99.99 })
state.isUpdated = true
})
State Subscription
Use $subscribe() to watch state changes; it triggers only once per patch (unlike watch):
catalogStore.$subscribe((mutation, state) => {
mutation.type // 'direct' | 'patch object' | 'patch function'
mutation.storeId // 'product-catalog'
mutation.payload // Patch object (only for 'patch object' type)
localStorage.setItem('productCatalog', JSON.stringify(state))
})
In Composable API, keep subscriptions active after component unmount:
<script setup>
import { useProductCatalog } from './stores/product-catalog'
const catalogStore = useProductCatalog()
catalogStore.$subscribe((mutation, state) => {
console.log('State updated:', state)
}, { detached: true })
</script>
Getters
Accessing Other Store Getters
Import and use other stores directly within a getter:
import { useUserProfile } from './user-profile'
export const useProductCatalog = defineStore('product-catalog', {
state: () => ({ products: [] }),
getters: {
availableProducts(state) {
const userStore = useUserProfile()
return state.products.filter(p => p.price <= userStore.maxBudget)
}
}
})
Passing Parameters to Getters
Return a function from a getter to accept parameters (note: this disables caching):
export const useProductCatalog = defineStore('product-catalog', {
state: () => ({ products: [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 699 }
] }),
getters: {
getProductById: (state) => (productId) =>
state.products.find(p => p.id === productId)
}
})
Component usage:
<script setup>
import { useProductCatalog } from './stores/product-catalog'
const catalogStore = useProductCatalog()
const product = catalogStore.getProductById(1)
</script>
<template>
<p>Product: {{ product?.name }}</p>
</template>
Actions
Accessing Other Store Actions
Import and call other store actions within an action:
import { useAuthSession } from './auth-session'
export const useOrderManager = defineStore('order-manager', {
state: () => ({ orderDetails: null }),
actions: {
async fetchUserOrders() {
const authStore = useAuthSession()
if (authStore.isLoggedIn) {
this.orderDetails = await fetch('/api/orders')
} else {
throw new Error('User not authenticated')
}
}
}
})
Action Subscriptions
Use $onAction() to listen to action calls, results, and errors:
const orderStore = useOrderManager()
const unsubscribe = orderStore.$onAction(({
name, store, args, after, onError
}) => {
const startTime = Date.now()
console.log(`Starting "${name}" with params: [${args.join(', ')}]`)
after(result => {
console.log(`"${name}" completed in ${Date.now() - startTime}ms
Result: ${result}`)
})
onError(error => {
console.warn(`"${name}" failed in ${Date.now() - startTime}ms
Error: ${error}`)
})
})
unsubscribe() // Manual cleanup
For persistent subscriptions (even after component unmount):
<script setup>
import { useOrderManager } from './stores/order-manager'
const orderStore = useOrderManager()
orderStore.$onAction(callback, true)
</script>
Plugins
Adding Global Store Properties
Create plugins to add shared properties to all stores:
import { createPinia } from 'pinia'
function GlobalStorePlugin() {
return { appVersion: '1.0.0', apiBaseUrl: '/api' }
}
const storeInstance = createPinia()
storeInstance.use(GlobalStorePlugin)
// Usage in another file
const anyStore = useAnyStore()
console.log(anyStore.appVersion) // '1.0.0'
Adding Per-Store and Shared Refs
Use ref() to add reactive properties; per-store properties are unique, shared refs are identical across stores:
import { ref } from 'vue'
const sharedRef = ref('Shared Value')
storeInstance.use(({ store }) => {
store.localProperty = ref('Unique Per Store')
store.sharedProperty = sharedRef
})
Adding Custom Store Options
Define new options when creating stores and use plugins to process them. For example, add a throttle option to limit action frequency:
// In store definition
import { defineStore } from 'pinia'
export const useProductCatalog = defineStore('product-catalog', () => {
// Setup store logic
return { products: [] }
}, {
throttle: { fetchProducts: 500 } // Throttle fetchProducts to 500ms
})
// Plugin implementation
import throttle from 'lodash/throttle'
storeInstance.use(({ options, store }) => {
if (options.throttle) {
return Object.keys(options.throttle).reduce((throttledActions, actionName) => {
throttledActions[actionName] = throttle(
store[actionName],
options.throttle[actionName]
)
return throttledActions
}, {})
}
})