Comprehensive Guide to Using Pinia for State Management in Vue
This article provides an in‑depth tutorial on Pinia, covering its origins, differences from Vuex, installation, store definition, state, getters, actions, plugin creation, persistence strategies, and practical code examples for integrating Pinia into Vue applications.
Introduction
Pinia (pronounced /piːnjʌ/) is a state‑management library for Vue.js, created by members of the Vue core team as the next iteration of Vuex. It offers a simpler API, Composition‑API style usage, and full TypeScript type inference.
Differences between Pinia and Vuex 3.x/4.x
Mutations are removed; only state , getters , and actions exist.
actions can modify state synchronously or asynchronously.
Built‑in TypeScript support with reliable type inference.
No module nesting – stores are independent and can call each other.
Plugin system enables easy extensions such as local storage.
Very lightweight – the compressed bundle is about 2 KB.
Basic Usage
Installation
Install Pinia via npm:
npm install piniaImport and create Pinia in main.js :
// src/main.js
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)Defining a Store
Create src/stores/counter.js and use defineStore() :
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: state => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})Alternatively, define the store with a setup function:
// src/stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})Using the Store in a Component
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// All three mutations are tracked by devtools
counterStore.count++
counterStore.$patch({ count: counterStore.count + 1 })
counterStore.increment()
</script>
<template>
<div>{{ counterStore.count }}</div>
<div>{{ counterStore.doubleCount }}</div>
</template>State
Destructuring a Store
Because a store is wrapped with reactive , direct destructuring loses reactivity. Use storeToRefs() instead:
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
</script>
<template>
<div>{{ count }}</div>
<div>{{ doubleCount }}</div>
</template>Modifying State
You can modify state directly ( store.count++ ) or use the higher‑performance $patch method to batch updates:
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
counterStore.$patch({
count: counterStore.count + 1,
name: 'Abalam'
})
</script>$patch also accepts a function for complex mutations:
cartStore.$patch(state => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})Listening to Store Changes
Use $subscribe() to react to any state change, similar to Vuex subscribe but with a single callback after multiple mutations:
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
counterStore.$subscribe((mutation, state) => {
// Persist state to localStorage on every change
localStorage.setItem('counter', JSON.stringify(state))
})
</script>You can also watch all stores via the Pinia instance:
import { watch } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
watch(
pinia.state,
state => {
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)Getters
Accessing Store Instance
Getters can reference this to access other getters within the same store:
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount(state) {
return state.count * 2
},
doublePlusOne() {
return this.doubleCount + 1
}
}
})Using Getters from Another Store
// src/stores/counter.js
import { defineStore } from 'pinia'
import { useOtherStore } from './otherStore'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 1 }),
getters: {
composeGetter(state) {
const otherStore = useOtherStore()
return state.count + otherStore.count
}
}
})Passing Parameters to a Getter
Return a function from a getter to accept arguments:
// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
users: [
{ id: 1, name: 'Tom' },
{ id: 2, name: 'Jack' }
]
}),
getters: {
getUserById: state => userId => state.users.find(user => user.id === userId)
}
})
// Component usage
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { getUserById } = storeToRefs(userStore)
</script>
<template>
<p>User: {{ getUserById(2) }}</p>
</template>Actions
Accessing Store Instance
Actions can use this and may be asynchronous:
// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({ userData: null }),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
} catch (error) {
return error
}
}
}
})Calling Actions of Another Store
// src/stores/setting.js
import { defineStore } from 'pinia'
import { useAuthStore } from './authStore'
export const useSettingStore = defineStore('setting', {
state: () => ({ preferences: null }),
actions: {
async fetchUserPreferences(preferences) {
const authStore = useAuthStore()
if (authStore.isAuthenticated()) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated!')
}
}
}
})Plugins
Pinia’s plugin system allows you to extend stores at a low level. A plugin is a function that receives a context object containing app , pinia , store , and options . It can return an object whose properties are merged into the store’s state or actions.
export function myPiniaPlugin(context) {
// context.app – Vue 3 app instance
// context.pinia – Pinia instance
// context.store – store being extended
// context.options – options passed to defineStore()
return {
hello: 'world', // adds state property
changeHello() { // adds action
this.hello = 'pinia'
}
}
}Register the plugin:
// src/main.js
import { createPinia } from 'pinia'
import { myPiniaPlugin } from './myPlugin'
const pinia = createPinia()
pinia.use(myPiniaPlugin)Adding New State via Plugin
pinia.use(() => ({ hello: 'world' })) import { ref, toRef } from 'vue'
pinia.use(({ store }) => {
const hello = ref('word')
store.$state.hello = hello
store.hello = toRef(store.$state, 'hello')
})Adding Custom Options
You can define custom options (e.g., debounce ) in a store and let a plugin act on them:
// src/stores/search.js
import { defineStore } from 'pinia'
export const useSearchStore = defineStore('search', {
actions: {
searchContacts() {},
searchContent() {}
},
debounce: {
searchContacts: 300,
searchContent: 500
}
}) // src/main.js
import { createPinia } from 'pinia'
import { debounce } from 'lodash'
const pinia = createPinia()
pinia.use(({ options, store }) => {
if (options.debounce) {
return Object.keys(options.debounce).reduce((acc, action) => {
acc[action] = debounce(store[action], options.debounce[action])
return acc
}, {})
}
})Persisting State
Install the persistence plugin and enable it in a store:
npm i pinia-plugin-persist // src/main.js
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(piniaPluginPersist) // src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 1 }),
persist: { enabled: true }
})You can customize the storage key, target storage, and which state paths to persist:
persist: {
enabled: true,
strategies: [
{
key: 'myCounter',
storage: localStorage,
paths: ['name', 'age']
}
]
}Conclusion
Pinia is lighter and simpler than Vuex while offering richer features, TypeScript support, and a powerful plugin system that can handle tasks such as debouncing actions and persisting state. It can also be used in Vue 2 via the map helpers.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.