Mastering Pinia: A Complete Guide to Vue 3 State Management
This comprehensive tutorial walks through Pinia's core concepts—including stores, defineStore, reactive integration, devtools, plugins, TypeScript support, SSR handling, and mapping helpers—providing step‑by‑step code examples and detailed explanations for building robust Vue 3 applications.
1. Store
In Pinia a Store encapsulates application state and business logic. It consists of three core parts:
state : a reactive object that holds data.
getters : computed‑like functions that derive cached values from state.
actions : methods (sync or async) that modify state or perform side effects.
Example: User Store
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
users: [] // initial empty array
}),
getters: {
// number of users in the array
count: (state) => state.users.length
},
actions: {
// add a user object to the array
addUser(user) {
this.users.push(user)
}
}
})Component usage (Composition API with <script setup>):
<template>
<div>
<button @click="addNewUser">Add User</button>
<p>Total users: {{ userCount }}</p>
<ul>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
const store = useUserStore()
const users = computed(() => store.users)
const userCount = computed(() => store.count)
function addNewUser() {
store.addUser({ id: Date.now(), name: 'New User' })
}
</script>2. defineStore()
defineStore()creates a Pinia store by providing a unique identifier and a configuration object that may contain state, getters, and actions.
Example: Cart Store
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] // initial empty cart
}),
getters: {
// total quantity of items in the cart
itemCount(state) {
return state.items.reduce((total, item) => total + item.quantity, 0)
}
},
actions: {
// add an item to the cart (merge if exists)
addItem(item) {
const index = this.items.findIndex(i => i.id === item.id)
if (index > -1) {
this.items[index].quantity += item.quantity
} else {
this.items.push(item)
}
},
// empty the cart
clearCart() {
this.items = []
}
}
})Component usage:
<template>
<div>
<button @click="addItem">Add Item</button>
<button @click="clearCart">Clear Cart</button>
<p>Total items: {{ itemCount }}</p>
<ul>
<li v-for="item in cartItems" :key="item.id">
{{ item.name }} - Qty: {{ item.quantity }}
</li>
</ul>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
const store = useCartStore()
const cartItems = computed(() => store.items)
const itemCount = computed(() => store.itemCount)
function addItem() {
store.addItem({ id: 1, name: 'Product A', quantity: 1 })
}
function clearCart() {
store.clearCart()
}
</script>3. reactive()
reactive()(Vue 3 Composition API) creates a deeply reactive object. Pinia stores typically use reactive() for their state so that any mutation triggers component updates.
Example: Reactive Store
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useMyStore = defineStore('myStore', {
state: () => reactive({
count: 0,
message: 'Hello, Pinia!'
})
// getters and actions can be added here
})Component usage:
<template>
<div>
<p>Count: {{ count }}</p>
<p>Message: {{ message }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useMyStore } from '@/stores/myStore'
const store = useMyStore()
const count = computed(() => store.state.count)
const message = computed(() => store.state.message)
function increment() {
store.state.count++
}
</script>4. Devtools Support
Pinia integrates with the Vue Devtools extension, providing state inspection, time‑travel debugging, action tracking, and direct state mutation.
Setup
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')Example store used for debugging:
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})Component usage:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
const count = computed(() => store.count)
function increment() { store.increment() }
</script>In Vue Devtools you can:
Inspect the state of each Pinia store.
View the history of actions and their parameters.
Perform time‑travel by reverting to previous state snapshots.
Edit state directly and see live updates.
5. Plugin System
Pinia plugins can extend store functionality by hooking into the store creation process. They can add custom properties, modify state/getters/actions, or run cleanup logic.
Example Plugin
function myPiniaPlugin(pinia) {
// Register a callback that runs for every store
pinia.use((store) => {
// Add a custom property to each store instance
store.myCustomProperty = 'Hello from plugin!'
})
}Register the plugin when creating the Pinia instance:
import { createPinia } from 'pinia'
const pinia = createPinia().use(myPiniaPlugin)Define a simple store that will receive the plugin property:
import { defineStore } from 'pinia'
export const useMyStore = defineStore('myStore', {
state: () => ({ value: 0 })
})Component usage (accessing the plugin‑added property):
<template>
<div>
<p>Value: {{ value }}</p>
<p>Plugin Property: {{ store.myCustomProperty }}</p>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useMyStore } from '@/stores/myStore'
const store = useMyStore()
const value = computed(() => store.value)
</script>6. TypeScript Support
Pinia provides first‑class TypeScript integration: type inference, declaration files, auto‑completion, and type guards.
Typed Store Example
import { defineStore } from 'pinia'
interface State {
count: number
message: string
}
export const useTypedStore = defineStore('typedStore', {
state: (): State => ({
count: 0,
message: 'Hello, Pinia with TypeScript!'
}),
getters: {
doubleCount: (state): number => state.count * 2
},
actions: {
increment(): void {
this.count++
}
}
})Component (TypeScript lang="ts"):
<template>
<div>
<p>Count: {{ count }}</p>
<p>Message: {{ message }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTypedStore } from '@/stores/typedStore'
const store = useTypedStore()
const count = computed(() => store.count)
const message = computed(() => store.message)
const doubleCount = computed(() => store.doubleCount)
function increment() {
store.increment()
}
</script>Type guard example (ensures property exists before use):
if (store.hasOwnProperty('count')) {
// TypeScript now knows 'count' is a number
console.log(store.count)
}7. SSR Support
Pinia can serialize its state on the server, transfer it to the client, and restore it, enabling server‑side rendering (SSR) for faster initial loads and SEO.
Store Definition (shared between server and client)
import { defineStore } from 'pinia'
export const useSSRStore = defineStore('ssrStore', {
state: () => ({ count: 0 })
})Server‑side creation and serialization
// server.js
import { createPinia } from 'pinia'
import { useSSRStore } from '@/stores/ssrStore'
const pinia = createPinia()
const store = useSSRStore(pinia)
// Simulate fetching initial data (e.g., from a database)
store.$state.count = 10
// Serialize the state to JSON for injection into the HTML payload
const serializedState = JSON.stringify(store.$state)
// The serializedState should be embedded in the rendered page (e.g., window.__PINIA_STATE__)Client‑side restoration
// client.js
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
import { useSSRStore } from '@/stores/ssrStore'
const pinia = createPinia()
// Assume the server injected a global variable with the serialized state
const stateFromServer = JSON.parse(window.__PINIA_STATE__)
const store = useSSRStore(pinia)
store.$state = stateFromServer // restore the exact state
const app = createApp(App)
app.use(pinia)
app.mount('#app')Component usage after restoration:
<template>
<div>{{ count }}</div>
</template>
<script setup>
import { computed } from 'vue'
import { useSSRStore } from '@/stores/ssrStore'
const store = useSSRStore()
const count = computed(() => store.count)
</script>8. Mapping Helper Functions
Pinia provides mapState, mapGetters, and mapActions to bind store properties to component computed refs, methods, or reactive values, reducing boilerplate.
Example Store
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: {
itemCount: (state) => state.items.length
},
actions: {
addItem(item) {
this.items.push(item)
}
}
})Component using mapping helpers
<template>
<div>
<p>Item Count: {{ itemCount }}</p>
<button @click="addItem({ id: 1, name: 'Apple' })">Add Apple</button>
</div>
</template>
<script setup>
import { mapState, mapGetters, mapActions } from 'pinia'
import { useCartStore } from '@/stores/cart'
const store = useCartStore()
// mapState returns a computed ref for the requested state key
const items = mapState(store, 'items')
// mapGetters returns a computed ref for the getter
const itemCount = mapGetters(store, 'itemCount')
// mapActions returns bound action functions
const { addItem } = mapActions(store, ['addItem'])
</script>These helpers keep component code concise while preserving reactivity: changes to items automatically update itemCount and any UI bound to them.
Java Architecture Stack
Dedicated to original, practical tech insights—from skill advancement to architecture, front‑end to back‑end, the full‑stack path, with Wei Ge guiding you.
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.
