Store (Pinia)
Store (Pinia)
Level 7 — State Management & Pinia An isolated, self-contained domain module in Pinia holding reactive State, memoized Getters, and imperative Actions for a specific application feature.
1. Prerequisites
- Pinia — The state management library powering Pinia Store modules.
- Composition API — The functional programming paradigm used to define modern Pinia Setup Stores.
2. Term Category
Vue Ecosystem Construct (Pinia Store Module Container): A Pinia Store is a modular unit of application state created using defineStore(). Operating across client and server environments, each store represents an independent domain namespace (e.g., useUserStore, useCartStore, useThemeStore).
Unlike legacy state management libraries (like Vuex) that maintained a single global state object, Pinia Stores are completely modular and decentralized. Stores are instantiated lazily when invoked by components, composables, or router guards. They integrate directly with Vue DevTools, supporting time-travel debugging, action inspection, and hot module replacement (HMR) during local development.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In Vuex 3/4, application state was defined as a single monolithic tree (store.state.user.profile.name). Splitting a monolithic store required registering sub-modules with string namespaces (dispatch('user/profile/update')), creating nested paths that were difficult to refactor and impossible to type-check cleanly in TypeScript.
Pinia introduced Modular Stores using the defineStore() function. Each store is defined in its own file with a unique string ID (e.g., 'user'). Stores act like specialized composables: you export a custom hook like export const useUserStore = defineStore('user', () => { ... }). When a component needs user data, it imports useUserStore and calls it directly. This modular structure provides clean code splitting, automatic tree-shaking for unused stores, and seamless TypeScript auto-completion.
(2) Reality Metaphor
Imagine a modern smartphone operating system. Instead of maintaining one massive 500-page settings ledger on a physical clipboard, the OS provides dedicated App Containers (Stores).
The Battery Manager app (useBatteryStore) manages battery percentages and power-saving modes. The Wi-Fi Manager app (useWifiStore) manages SSID connections and passwords. The Apps operate independently: opening Wi-Fi settings does not load or evaluate Battery Manager memory. When an app needs permission data from another service, it calls the system API directly. Each store module is isolated, lightweight, and loaded on demand.
(3) Vue Code Examples
Short Snippet
<script setup>
import { defineStore } from 'pinia'
import { ref } from 'vue'
// Define a minimal Pinia Setup Store with unique ID 'counter'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() { count.value++ }
return { count, increment }
})
</script>
Fuller Example
<script setup>
import { defineStore, storeToRefs } from 'pinia'
import { ref, computed } from 'vue'
// Define domain Store module for shopping cart
export const useCartStore = defineStore('cart', () => {
// STATE
const items = ref([])
// GETTERS
const itemCount = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0))
const totalPrice = computed(() => items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0))
// ACTIONS
function addItem(product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(productId) {
items.value = items.value.filter(i => i.id !== productId)
}
return { items, itemCount, totalPrice, addItem, removeItem }
})
// Component usage
const cartStore = useCartStore()
// Extract reactive getters safely
const { itemCount, totalPrice } = storeToRefs(cartStore)
// Extract actions directly
const { addItem, removeItem } = cartStore
</script>
<template>
<div class="cart-widget">
<h3>Shopping Cart (Items: {{ itemCount }})</h3>
<p>Total: ${{ totalPrice.toFixed(2) }}</p>
<ul>
<li v-for="item in cartStore.items" :key="item.id">
{{ item.name }} x {{ item.quantity }}
<button @click="removeItem(item.id)">Remove</button>
</li>
</ul>
</div>
</template>
4. Common Mistakes & Pitfalls
Mistake 1: Duplicate Store String ID Collisions
The mistake: Registering two separate store definitions with the exact same string ID parameter: defineStore('user', ...) and defineStore('user', ...).
Why it's wrong: Pinia uses the unique string ID as a lookup key in the global Pinia registry and DevTools. Duplicate store IDs overwrite each other, leading to state corruption and silent hydration failures in SSR applications.
Incorrect:
export const useUserStore = defineStore('user', () => { ... })
export const useProfileStore = defineStore('user', () => { ... }) // ❌ Duplicate ID 'user'!
Fix:
export const useUserStore = defineStore('user', () => { ... })
export const useProfileStore = defineStore('profile', () => { ... }) // Unique string IDs
Mistake 2: Forgetting to Return State from Setup Stores
The mistake: Declaring const count = ref(0) inside a setup store defineStore('counter', () => { ... }) but omitting count from the returned object.
Why it's wrong: Setup stores work exactly like Composition API setup functions. Only properties explicitly included in the returned object (return { count, increment }) are exposed on the store instance. Private un-returned refs remain hidden.
Incorrect:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() { count.value++ }
return { increment } // ❌ count is omitted from public store API!
})
Fix:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() { count.value++ }
return { count, increment } // Expose state ref in return object
})
Mistake 3: Destructuring Store State Directly Without storeToRefs()
The mistake: Extracting state variables directly using ES6 destructuring: const { count } = useCounterStore().
Why it's wrong: A Pinia store instance is a reactive Proxy. Destructuring properties extracts raw primitive copies, breaking reactivity. Always use storeToRefs(store) for state and getters.
Incorrect:
const store = useCounterStore()
const { count } = store // ❌ Destructuring breaks reactivity!
Fix:
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count } = storeToRefs(store) // Preserves reactive ref binding
5. Practice Exercises
Exercise 1: IoT Warehouse Fleet Store
Scenario: A robotics warehouse system requires a Pinia Store module useRobotStore to manage autonomous forklift locations, battery levels, and task assignments.
Requirements:
- Define store ID
'robots'. - State
robotsarray containing{ id, status, battery, location }. - Getter
lowBatteryRobotsreturning array of robots withbattery < 20. - Action
assignTask(robotId, task)updating target robot status to'busy'.
Answer
Implementation
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useRobotStore = defineStore('robots', () => {
const robots = ref([
{ id: 'BOT-01', status: 'idle', battery: 85, location: 'Aisle 3' },
{ id: 'BOT-02', status: 'idle', battery: 14, location: 'Aisle 7' }
])
const lowBatteryRobots = computed(() => {
return robots.value.filter(r => r.battery < 20)
})
function assignTask(robotId, task) {
const bot = robots.value.find(r => r.id === robotId)
if (!bot) throw new Error(`Robot ${robotId} not found`)
if (bot.battery < 20) throw new Error(`Robot ${robotId} battery too low for task`)
bot.status = 'busy'
bot.currentTask = task
}
return { robots, lowBatteryRobots, assignTask }
})
Technical Explanation
- Setup Store Definition:
defineStore('robots', ...)exports custom hook function returning reactive state and getters. - Computed Filter Getter:
lowBatteryRobotstracksrobotsarray mutations and evaluates low-battery items dynamically. - Action Boundary Guards:
assignTaskvalidates battery thresholds before mutating robot status state. - Proxy Reactivity: Updating
bot.statusmutates the reactive proxy object inside the array seamlessly.
Exercise 2: Financial Currency Rates Store with Auto-Refresh
Scenario: A trading application needs a Pinia Store useFxStore that fetches currency exchange rates from an API and updates rates on a periodic interval.
Requirements:
- Store ID
'fxRates'. - State
ratesobject{ USD: 1.0, EUR: 0.92, GBP: 0.79 }andlastUpdatedtimestamp ref. - Action
fetchRates()fetching fresh rate data asynchronously. - Provide action
startAutoRefresh(intervalMs)returning a cleanup stop function.
Answer
Implementation
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useFxStore = defineStore('fxRates', () => {
const rates = ref({ USD: 1.0, EUR: 0.92, GBP: 0.79 })
const lastUpdated = ref(Date.now())
let timerId = null
async function fetchRates() {
try {
const res = await fetch('/api/fx-rates')
if (res.ok) {
rates.value = await res.json()
lastUpdated.value = Date.now()
}
} catch (e) {
console.error('FX Fetch failed', e)
}
}
function startAutoRefresh(intervalMs = 5000) {
if (timerId) clearInterval(timerId)
timerId = setInterval(() => {
fetchRates()
}, intervalMs)
return () => {
if (timerId) clearInterval(timerId)
}
}
return { rates, lastUpdated, fetchRates, startAutoRefresh }
})
Technical Explanation
- Private Timer Variable:
timerIdis kept private inside setup store closure, un-returned in public store API object. - Async Refresh Action:
fetchRatesexecutes network dispatch and updates timestamp refs. - Interval Cleanup Callback:
startAutoRefreshreturns explicit cleanup function for component lifecycle unmounting hooks. - State Isolation: Currency rates are isolated from transaction stores, ensuring modular reusability.
Exercise 3: E-Commerce Persistent User Wishlist Store
Scenario: An online storefront requires a useWishlistStore that tracks saved product IDs and persists changes to browser localStorage.
Requirements:
- Store ID
'wishlist'. - State
wishlistIdsarray initialized fromlocalStorage. - Getter
wishlistCountreturning total saved items. - Action
toggleWishlist(productId)adding or removing product IDs.
Answer
Implementation
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useWishlistStore = defineStore('wishlist', () => {
const wishlistIds = ref(JSON.parse(localStorage.getItem('wishlist_ids') || '[]'))
const wishlistCount = computed(() => wishlistIds.value.length)
function toggleWishlist(productId) {
const index = wishlistIds.value.indexOf(productId)
if (index > -1) {
wishlistIds.value.splice(index, 1)
} else {
wishlistIds.value.push(productId)
}
localStorage.setItem('wishlist_ids', JSON.stringify(wishlistIds.value))
}
return { wishlistIds, wishlistCount, toggleWishlist }
})
Technical Explanation
- Initial Ref Hydration:
wishlistIdsparses browser storage fallback during store instantiation. - Toggle Mutator Action:
toggleWishlistuses.indexOf()and.splice()to add/remove items reactively. - Sync Persistence: Updates write directly to
localStoragekey synchronously after state mutations. - Derived Count Getter:
wishlistCountupdates badge counts automatically across navigation icons.
6. Related Terms
- Pinia — The parent state management library powering Pinia Store modules.
- State & Getters (Pinia) — The data structures defined inside a store.
- Actions (Pinia) — The functions defined inside a store.
- State Management — The overall frontend architecture pattern.
- Composables — Composition API logic functions sharing structural patterns with setup stores.
7. Key Takeaways
- A Pinia Store is an isolated, modular domain container defined via
defineStore('id', () => { ... }). - Modern stores use Setup Store syntax, matching Composition API functions.
- Every store in an application must have a unique string ID parameter.
- Remember to return all state, getter, and action properties that should be public.
- Use
storeToRefs()when destructuring reactive state from a store instance.