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:
MenuListadded items.SiteHeadershowed the new count. - No props or events were passed between them.
What You Will Learn
- When a store is the right tool.
- How to install Pinia.
- Option stores: state, getters and actions.
- Setup stores, built like composables.
- How to read a store safely with
storeToRefs(). - How to change many values with
$patch()and reset a store. - Async actions with loading and errors.
- Stores that use other stores, and stores outside components.
$subscribe,$onAction, saving tolocalStorageand plugins.
When to Use a Store
| State | Where it lives |
|---|---|
| Used by one component | A ref in that component |
| Shared by a parent and a few children | Props and events |
| Shared down one branch | provide / 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:
$patchchanged 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 ownreset()action.
Reading a Store Safely
- The store is a
reactiveobject. 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
loadinganderrorin 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 afterapp.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
localStoragein$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-persistedstateand addpersist: trueto 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 withapp.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. Writereset()in setup stores.$subscribe/$onAction= after state changes / when actions run.pinia.use(plugin)= adds features, such as saving, to every store.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Destructuring the store directly | const { count } = store stays live | Use storeToRefs(store) | The value freezes |
| Everything in a store | Stores are always better | Keep local state local | Hard-to-follow global state |
$reset() on a setup store | Every store has it | Write a reset() action | An error |
Calling a store before app.use(pinia) | Stores work anywhere | Call it after Pinia is installed | "getActivePinia was called with no active Pinia" |
| Changing state from far-away components | Any component may edit freely | Change state through actions | Hard to find who changed what |
| Saving the whole state on every keystroke to a server | $subscribe is free | Debounce, or save in actions | Too 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. $patchchanges many values.$resetis for option stores only.$subscribe,$onActionand plugins watch, log and save stores.
Next: HTTP Requests