codejump

AcademyVue

Pinia: State Management for Vue Apps

A Pinia store is a composable with a fixed shape — state, getters and actions — and understanding it as one demystifies most of what it does.

2 min readUpdated

3 exercises

Waiting at the end of this page.

A store is state, getters and actions

Pinia's defineStore composition-API form is a composable with three conventional parts, returned as one object:

JavaScript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // state
  const items = ref([])

  // getters — derived, cached like any computed
  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0),
  )

  // actions — the only place state should change
  function addItem(item) {
    items.value.push(item)
  }

  return { items, total, addItem }
})

Called from any component as const cart = useCartStore(), every caller gets the same store instance — unlike a plain composable, where each call creates independent state, defineStore registers the store once and hands back the same reactive object every time. That single shared instance, not anything about ref/computed themselves, is the entire mechanism behind "global state" in Pinia.

Why Pinia replaced Vuex

Vuex required a separate mutations layer purely so Vue DevTools could time-travel through state changes — every state update had to go through a mutation, and every async operation had to live in a separate action that committed a mutation when it finished. Pinia's reactivity is the same ref/reactive system every other part of Vue 3 uses, so DevTools can already observe changes directly; the mutations layer was scaffolding the new reactivity system no longer needs. An action in Pinia can be async and mutate state directly, in one function:

JavaScript
async function checkout() {
  isLoading.value = true
  try {
    await api.post('/orders', { items: items.value })
    items.value = []
  } finally {
    isLoading.value = false
  }
}

Destructuring a store loses reactivity the same way reactive() does

A store built with the composition-API syntax above is, under the hood, a reactive object — so pulling a property out with plain destructuring copies the current value and disconnects it from future updates, exactly the reactive trap from earlier in this path:

JavaScript
const cart = useCartStore()
const { total } = cart          // frozen at whatever total was right now

import { storeToRefs } from 'pinia'
const { total } = storeToRefs(cart)   // stays connected — Pinia's toRefs

storeToRefs exists for exactly this, and it only wraps state and getters — actions are plain functions and can be destructured directly (const { addItem } = cart is always safe, since a function has no reactive value to lose).

What interviewers check

Whether you know why Pinia dropped mutations rather than assuming it is "Vuex but newer" — and whether storeToRefs comes up unprompted the moment a store gets destructured, since forgetting it is the most common Pinia bug in production code.

Now practice it

3 exercises

Reading this page is the cheap half. These are the exercises that make you use it.