State Management with Pinia

By Syncfusion®, Inc. · Last updated

Local vs shared state and when to use a store, createPinia(), option stores and setup stores, state, getters and actions, async actions with errors, storeToRefs(), $patch, $reset and resetting setup stores, stores using stores, stores outside components, $subscribe, $onAction, persistence and Pinia plugins.

Shared State

  • Local state belongs to one component, such as whether a dropdown is open.
  • Shared state is needed by many components far apart, such as the cart.
  • Props and events get messy for shared state. You pass data through components that do not need it.
  • A store = one place, outside any component, that holds shared state and the code that changes it.
  • Pinia = the official store library for Vue.

Simple lesson: put state that many far-apart components need into a Pinia store.

A Real-World Example

  • Meera's menu has Add buttons. Her header shows the cart count.
  • The two are far apart in the component tree.
  • A cart store holds the items. Both components use it.
  • What happened: MenuList added items. SiteHeader showed the new count.
  • No props or events were passed between them.

What You Will Learn

  1. When a store is the right tool.
  2. How to install Pinia.
  3. Option stores: state, getters and actions.
  4. Setup stores, built like composables.
  5. How to read a store safely with storeToRefs().
  6. How to change many values with $patch() and reset a store.
  7. Async actions with loading and errors.
  8. Stores that use other stores, and stores outside components.
  9. $subscribe, $onAction, saving to localStorage and plugins.

When to Use a Store

StateWhere it lives
Used by one componentA ref in that component
Shared by a parent and a few childrenProps and events
Shared down one branchprovide / inject
Shared across the whole app (user, cart, settings)A Pinia store

Animation: the cart store sits in the middle. MenuList calls the add action, the store's state changes, and SiteHeader and CartPage both update

cart store items, count, add()

MenuList SiteHeader CartPage

One component calls an action. Every component that reads the store updates.

Installing Pinia

  • In a real project: npm install pinia. create-vue can add it for you.
  • In main.ts: app.use(createPinia()). Pinia is a plugin.
  • Put stores in a stores/ folder, one file per store.
  • Name the function useXStore, like a composable.

Option Stores

  • defineStore("id", { state, getters, actions }) makes an option store.
  • state = a function returning the starting data.
  • getters = computed values from the state.
  • actions = methods that change the state. They can be async.
  • Inside getters and actions, use this, as in the Options API.
  • What happened: $patch changed two values in one step.
  • $reset() put the starting state back. It works for option stores only.

Simple lesson: option stores have state, getters and actions.

Vue DevTools has a Pinia tab. It shows every store, its state and a timeline of actions, and lets you edit state while the app runs.

Setup Stores

  • defineStore("id", () => { ... }) makes a setup store.
  • It is written like a composable: ref = state, computed = getters, functions = actions.
  • Return every piece of state. State you keep private is missed by DevTools, SSR and plugins.
  • It can use watchers and other composables.
  • $reset() does not exist here. Write your own reset() action.

Reading a Store Safely

  • The store is a reactive object. Destructuring it breaks reactivity, as you learned in Reactive State.
  • storeToRefs(store) turns state and getters into refs that stay linked.
  • Take actions straight from the store. They are plain functions.

const { guests } = useBookingStore() gives a frozen copy. Always destructure state and getters with storeToRefs(). Actions can be destructured directly.

Simple lesson: setup stores are composables with a name. Read them with storeToRefs().

Async Actions

  • Actions can be async. Use them to load and save data.
  • Keep loading and error in the store, so every component can show them.
  • Catch errors in the action, or let the caller handle them.

Stores Together

  • Store in a store: call useCartStore() inside another store's action or getter.
  • Composables in a store: setup stores can call composables such as useFetch.
  • Outside components: call useXStore() in any function that runs after app.use(pinia), such as a router guard.

Watching and Saving a Store

  • store.$subscribe((mutation, state) => ...) runs after every state change.
  • store.$onAction(({ name, args, after, onError }) => ...) runs when an action is called.
  • To keep the cart after a reload, save it to localStorage in $subscribe, and read it back when the store starts.
  • A Pinia plugin (pinia.use(fn)) runs for every store. It is the usual place for saving.
  • Real projects often install pinia-plugin-persistedstate and add persist: true to a store instead of writing this by hand.

Real apps often use a ready plugin such as pinia-plugin-persistedstate. In this lab, each run starts with an empty in-memory localStorage, so saved data lasts until you press Run again.

Simple lesson: $subscribe sees changes, $onAction sees actions, plugins add features to every store.

TL;DR

  • Store = shared state and its logic, outside components.
  • createPinia() = install with app.use(createPinia()).
  • defineStore("id", {...}) = option store: state, getters, actions.
  • defineStore("id", () => {...}) = setup store: refs, computed, functions.
  • storeToRefs(store) = safe destructuring of state and getters.
  • $patch() = change many values at once.
  • $reset() = option stores only. Write reset() in setup stores.
  • $subscribe / $onAction = after state changes / when actions run.
  • pinia.use(plugin) = adds features, such as saving, to every store.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Destructuring the store directlyconst { count } = store stays liveUse storeToRefs(store)The value freezes
Everything in a storeStores are always betterKeep local state localHard-to-follow global state
$reset() on a setup storeEvery store has itWrite a reset() actionAn error
Calling a store before app.use(pinia)Stores work anywhereCall it after Pinia is installed"getActivePinia was called with no active Pinia"
Changing state from far-away componentsAny component may edit freelyChange state through actionsHard to find who changed what
Saving the whole state on every keystroke to a server$subscribe is freeDebounce, or save in actionsToo many requests

Check Yourself

Exercises

What to Remember

  • Use a store for app-wide shared state. Keep local state local.
  • Option stores: state, getters, actions. Setup stores: refs, computed, functions.
  • Destructure state with storeToRefs(). Actions can be destructured directly.
  • $patch changes many values. $reset is for option stores only.
  • $subscribe, $onAction and plugins watch, log and save stores.

Next: HTTP Requests