State management with Pinia

Vue · Practical · 13 min

Pinia is Vue's official store, used for anything many unrelated components need: session, cart, settings, caches.

When unrelated components need the same state, a store gives it one home. Pinia is the official choice. Install it with app.use(createPinia()) and define stores with defineStore.

The setup store syntax will feel familiar because it is a composable:

export const useCartStore = defineStore('cart', () => {
  const items = ref([])                         // state
  const total = computed(() => ...)             // getter
  function add(product) { items.value.push(product) }   // action
  return { items, total, add }
})

Every ref you return becomes state, computeds become getters and functions become actions. You must return all state for devtools and SSR to see it. (An "options store" with state, getters and actions keys also exists.)

In a component, call const cart = useCartStore(). The store is a reactive object, so access cart.items and cart.total without .value. All components share the same instance.

Destructuring a store loses reactivity. Use `storeToRefs(cart)` for state and getters; actions can be destructured directly.

Benefits over a hand-rolled module-level ref: devtools timeline and time travel, SSR safety (one store per request), hot module replacement, $reset/$patch/$subscribe helpers and plugins such as persisted state.

Keep stores focused (one per domain: useAuthStore, useCartStore) and put async calls in actions.

Example

// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  const total = computed(() => items.value.reduce((s, i) => s + i.price * i.qty, 0))
  function add(product) {
    const line = items.value.find(i => i.id === product.id)
    line ? line.qty++ : items.value.push({ ...product, qty: 1 })
  }
  async function checkout() {
    await fetch('/api/orders', { method: 'POST', body: JSON.stringify(items.value) })
    items.value = []
  }
  return { items, total, add, checkout }
})

// CartBadge.vue <script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
const { items, total } = storeToRefs(cart)   // still reactive
const { add } = cart                         // actions destructure fine

Remember

  • defineStore('id', () => { ... return {...} }) defines a setup store
  • Returned refs = state, computeds = getters, functions = actions; return all state
  • Use storeToRefs when destructuring state and getters

Watch out for

  • const { total } = useCartStore() gives a value that never updates; use storeToRefs.

Check yourself

  1. Why use Pinia instead of exporting a module-level ref for shared state in an SSR app?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pinia.vuejs.org/core-concepts/