State Management Pinia
After Alice adds an item to her cart, the cart is empty when she navigates to another page—each page has its own component state. Bob notices that the user's login status is inconsistent between SSR and the client-side. Charlie needs Pinia to manage shared state across components and pages.
1. What You'll Learn
- @pinia/nuxt Module Installation and Automatic Configuration
- Store Definition: Option-Based and Setup Syntax
- Mechanisms of Dehydration and Hydration in the SSR State
- StoreToRefs Reactive Destructuring and $patch/$reset
- MegaShop Shopping Cart: Add, Delete, Update, and Query + Total Price Calculation
2. A True Story from a Consumer
(1) Pain Point: "Lost" Shopping Cart Status
Alice added three items to her cart on the MegaShop homepage, but when she clicked to view the product details page, the cart icon showed 0 items—the status wasn't synced. Bob encountered a similar issue: after logging in and refreshing the page, his logged-in status disappeared.
(2) Solutions for Pinia State Management
Pinia decouples the shopping cart and user state from the component lifecycle, turning them into globally shared, reactive data:
// composables/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
return { items, total }
})
(3) Benefit: Cross-page state persistence
Alice's shopping cart remains consistent across all pages, and Bob's login status is now synchronized between SSR and the client, eliminating any issues with state loss.
3. Pinia Installation and Configuration
(1) Installation Steps
npm install @pinia/nuxt pinia
(1) ▶ Example: nuxt.config.ts configuration
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
// Pinia is auto-configured, no extra setup needed
})
Output:
// Execution Successful
(2) Comparison of Pinia, Vuex, and useState
| Dimension | Pinia | Vuex 4 | Nuxt useState |
|---|---|---|---|
| Vue 3 Support | ✅ Native | ⚠️ Compatibility Mode | ✅ Nuxt-Exclusive |
| TypeScript | ✅ Full type inference | ❌ Manual declaration required | ✅ Generic support |
| SSR Hydration | ✅ Automatic | ⚠️ Requires configuration | ✅ Automatic |
| Code Splitting | ✅ On-demand | ❌ Global | ❌ Global |
| DevTools | ✅ Supported | ✅ Supported | ❌ Not supported |
| Size | ~1 KB | ~6 KB | Built-in |
| Use Cases | Complex Scenarios | Legacy Migration | Simple Sharing |
4. How to Define a Store
(1) Option-Based Store
(1) ▶ Example: Option-Based Shopping Cart Store
// stores/cart.ts
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
couponCode: '' as string
}),
getters: {
totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
totalPrice: (state) => {
return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
formattedTotal(): string {
return new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD'
}).format(this.totalPrice)
}
},
actions: {
addItem(product: Product) {
const existing = this.items.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
},
removeItem(productId: number) {
this.items = this.items.filter(i => i.id !== productId)
},
updateQuantity(productId: number, quantity: number) {
const item = this.items.find(i => i.id === productId)
if (item) item.quantity = Math.max(0, quantity)
this.items = this.items.filter(i => i.quantity > 0)
},
clearCart() {
this.items = []
this.couponCode = ''
}
}
})
interface CartItem {
id: number; name: string; price: number; quantity: number; image: string
}
interface Product {
id: number; name: string; price: number; image: string
}
Output:
// Execution Successful
(2) Setup Syntax Store
(2) ▶ Example: Setup Syntax User Store
// stores/user.ts
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null)
const isAuthenticated = computed(() => !!user.value)
const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '')
// Actions
async function login(email: string, password: string) {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
user.value = response.user
}
function logout() {
user.value = null
}
async function fetchProfile() {
const profile = await $fetch('/api/user/profile')
user.value = profile
}
return {
user, isAuthenticated, fullName,
login, logout, fetchProfile
}
})
interface User {
id: number; email: string; firstName: string; lastName: string; role: 'customer' | 'admin'
}
Output:
// Execution Successful
(3) Comparison of the Two Methods
| Dimension | Option Type | Setup Syntax |
|---|---|---|
| Syntax | state/getters/actions | ref/computed/function |
| TypeScript | ⚠️ Interface declaration required | ✅ Automatic inference |
| Flexibility | ⚠️ Limited | ✅ Available in any Composable |
| SSR | ✅ Supports $reset | ⚠️ Does not support $reset (must be implemented manually) |
| Use Cases | Simple Store | Complex Store/Requires Composable |
5. SSR Dehydration and Hydration
(1) Pinia SSR State Transitions
flowchart LR
A[Server: Store filled with data] --> B[Serialize state to HTML payload]
B --> C[Client: Read payload]
C --> D[Hydrate Store with server state]
D --> E[Client: Store ready, no re-fetch]
(1) ▶ Example: Initializing the Store in SSR
// plugins/init-pinia.server.ts
export default defineNuxtPlugin(() => {
const cartStore = useCartStore()
// Initialize cart from cookie on server
const cartCookie = useCookie('cart-items')
if (cartCookie.value) {
cartStore.items = cartCookie.value
}
})
Output:
// Execution Successful
(2) ▶ Example: Differences Between Pinia and useState for SSR
| Dimension | Pinia | useState |
|---|---|---|
| SSR Hydration | ✅ Automatic payload | ✅ Automatic payload |
| Multi-Store | ✅ Independent Namespaces | ⚠️ Manual Key Management |
| DevTools | ✅ Visualization | ❌ None |
| Persistence | ✅ Plugin Support | ⚠️ Manual cookie required |
| Complex logic | ✅ actions/getters | ❌ ref only |
6. StoreToRefs and Batch Operations
(1) ▶ Example: StoreToRefs Reactive Destructuring
<script setup lang="ts">
const cartStore = useCartStore()
// ✅ Reactive destructure - keeps reactivity
const { items, totalPrice, totalItems } = storeToRefs(cartStore)
// ❌ Direct destructure - loses reactivity
// const { items, totalPrice } = cartStore
// Actions can be destructured directly (no reactivity needed)
const { addItem, removeItem, clearCart } = cartStore
</script>
Output:
// Execution Successful
(2) ▶ Example: $patch batch update
// Batch update with $patch - single reactivity trigger
const cartStore = useCartStore()
// Object style
cartStore.$patch({
couponCode: 'SAVE20',
items: [...cartStore.items, newItem]
})
// Function style (better for array mutations)
cartStore.$patch((state) => {
state.couponCode = 'SAVE20'
state.items.push(newItem)
state.items[0].quantity = 3
})
Output:
// Execution Successful
(3) ▶ Example: Shopping Cart Persistence Plugin
// plugins/pinia-persist.client.ts
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(({ store }) => {
// Load from localStorage on client
const saved = localStorage.getItem(`pinia-${store.$id}`)
if (saved) store.$patch(JSON.parse(saved))
// Save to localStorage on change
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
})
})
Output:
// Execution Successful
7. Comprehensive Example: MegaShop Shopping Cart System
<!-- pages/cart.vue -->
<template>
<div class="cart-page">
<h1>Shopping Cart</h1>
<div v-if="items.length === 0" class="empty-cart">
<p>Your cart is empty</p>
<NuxtLink to="/products">Continue Shopping</NuxtLink>
</div>
<div v-else>
<div class="cart-items">
<div v-for="item in items" :key="item.id" class="cart-item">
<img :src="item.image" :alt="item.name" />
<div class="details">
<h3>{{ item.name }}</h3>
<p>${{ item.price }} USD</p>
<div class="quantity">
<button @click="updateQuantity(item.id, item.quantity - 1)">-</button>
<span>{{ item.quantity }}</span>
<button @click="updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
</div>
<button @click="removeItem(item.id)" class="remove">Remove</button>
</div>
</div>
<div class="cart-summary">
<p>Items: {{ totalItems }}</p>
<p class="total">Total: {{ formattedTotal }}</p>
<button @click="clearCart" class="clear">Clear Cart</button>
<NuxtLink to="/checkout" class="checkout">Proceed to Checkout</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const cartStore = useCartStore()
const { items, totalItems, formattedTotal } = storeToRefs(cartStore)
const { removeItem, updateQuantity, clearCart } = cartStore
</script>
❓ FAQ
storeToRefs to deconstruct while maintaining reactivity; actions can be deconstructed directly (functions do not require reactivity).onMounted (values modified during the SSR phase will be overwritten during hydration). Pinia's SSR hydration is automatic and typically does not require manual handling.$reset not available in the Setup syntax?$reset. You can implement this manually by defining a $reset action or resetting all refs in $patch.📖 Summary
- Pinia is the official state management solution for Vue 3; the @pinia/nuxt module provides automatic integration
- The option-based Store has a clear structure (state/getters/actions), and the Setup syntax is more flexible
- SSR Hydration Auto-Completion: Server-side Store data → payload → client-side Store
storeToRefskeeps destructuring reactive;$patchtriggers only one render even with batch updates- MegaShop uses Pinia to manage shopping carts and user status, and relies on cookies for persistence
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Create
useCartStore, implementaddItemandremoveItem, manipulate the shopping cart on two different pages, and verify that the state is shared. - Advanced Exercise (Difficulty: ⭐⭐): Use the Setup syntax to define
useUserStore, implementlogin,logout, andfetchProfile, and verify that SSR hydration works correctly. - Challenge (Difficulty: ⭐⭐⭐): Implement shopping cart persistence (so data is retained after refreshing the page), and compare the pros and cons of using the localStorage plugin versus cookies for synchronization.
---|



