Vue.js 53 🟢 Resetting, Subscribing to State ($subscribe), and Actions ($onAction) in Pinia
A Pinia store is not just a passive container. It exposes three powerful methods that let you control its lifecycle and react to its activity: $reset(), $subscribe(), and $onAction(). These methods are what turn a store from a simple data holder into an observable, resettable, and instrumentable part of your application’s architecture.
The most important distinction to internalize is that $reset() is not universal. It works out of the box for Option Stores, but Setup Stores do not have a built-in $reset()—calling it throws an error telling you the store was built using setup syntax and does not implement it . This asymmetry is deliberate, reflecting the different philosophies of the two syntaxes, but it catches many developers off guard when they migrate from one to the other.
$subscribe() and $onAction() are the observation hooks. $subscribe() fires whenever the store’s state changes, giving you the mutation type and the current state . $onAction() fires whenever an action is about to be invoked, giving you the action’s name and arguments, plus after and onError callbacks for completion and failure . Together, these two methods are the foundation for persistence plugins, audit logs, and cross-store coordination.
This chapter covers each method, the critical difference between Option and Setup Stores for $reset(), the detached option that controls subscription lifetime, and the known limitation of $onAction() with Setup Stores.
Key point: $reset() restores state to its initial values but exists only on Option Stores by default; $subscribe() observes every state change and fires a callback with the mutation and state; $onAction() observes action invocations and provides after and onError hooks. Both $subscribe() and $onAction() are automatically cleaned up when the component unmounts unless detached: true is passed.
Why these three methods exist
The reset problem. Applications need to clear state. A user logs out, and the cart must empty; a form is submitted, and the draft must clear; a test runs, and the store must return to a known baseline. Without $reset(), you would manually assign every state property back to its initial value—a maintenance burden that grows with every new field . For Option Stores, Pinia handles this automatically. For Setup Stores, you must either implement it yourself or use a plugin.
The persistence problem. State is ephemeral by default. A page refresh wipes everything. The standard solution is to subscribe to state changes and write the state to localStorage or sessionStorage on every mutation . $subscribe() provides exactly this hook: it fires after every state change, with the current state as an argument. Without it, you would need to wrap every action in persistence logic.
The instrumentation problem. Sometimes you need to know when an action is called, not just when state changes. An audit log needs the action name and arguments. A loading indicator needs to know when an action starts and finishes. $onAction() provides this: it fires before the action runs, and the after callback fires when it completes successfully or the onError callback fires when it fails . This is finer-grained than $subscribe(), which only sees the resulting state change.
The lifecycle problem. Subscriptions created inside a component are automatically cleaned up when the component unmounts. This is usually desirable—a component that is gone should not keep reacting to store changes. But sometimes you need a subscription that outlives the component: a global persistence layer, a logging plugin, or a cross-store coordination mechanism. The detached: true option exists for exactly this case .
The Setup Store limitation. $onAction() works by wrapping the store’s actions at the time the store is defined. In Setup Stores, actions are plain functions returned from the setup function. When one action calls another action defined in the same store, the call bypasses the $onAction() wrapper because it is a direct function call, not a property access on the store object . This is a known limitation that affects cross-action instrumentation in Setup Stores.
a. $reset(): Option Stores vs Setup Stores
Calling $reset() on an Option Store is straightforward. It restores the state to the values returned by the state function.
const store = useCounterStore()
store.count = 10
store.$reset()
// store.count is back to 0
The mechanism works because Pinia captures the initial state when the store is created and can replay it. For Option Stores, this is automatic .
For Setup Stores, there is no built-in $reset(). Calling it throws:
🍍: Store "xxx" is built using the setup syntax and does not implement $reset().
You must implement it yourself:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Eduardo')
function $reset() {
count.value = 0
name.value = 'Eduardo'
}
return { count, name, $reset }
})
The manual implementation is tedious and error-prone—every new state property must be added to the reset function. An alternative is a plugin that captures the initial state and injects $reset() automatically. A common pattern is to deep-clone store.$state when the store is created, then use $patch() to restore it . The pinia-plugin-setup-reset package does exactly this, and adds optional selective reset: store.$reset('name') resets only the name property .
b. $subscribe(): observing state changes
$subscribe() registers a callback that fires after every state change. The callback receives two arguments: a mutation object and the current state.
store.$subscribe((mutation, state) => {
console.log(mutation.type) // 'direct' | 'patch object' | 'patch function'
console.log(mutation.storeId) // the store's id
console.log(mutation.payload) // only for 'patch object'
console.log(state) // the current state
})
The mutation type tells you how the state changed. 'direct' means a direct assignment (store.count = 1). 'patch object' means $patch({ count: 1 }). 'patch function' means $patch((state) => { state.count = 1 }) . This distinction is useful for debugging and for filtering which changes you care about.
The most common use case is persistence. The pattern is to subscribe once, and on every change, serialize the state to storage:
store.$subscribe((mutation, state) => {
localStorage.setItem('cart', JSON.stringify(state))
})
On initialization, the store reads from storage:
state: () => ({
items: JSON.parse(localStorage.getItem('cart') || '[]')
})
This combination—subscribe to write, initialize to read—is the manual persistence pattern . Libraries like pinia-plugin-persistedstate wrap this into a declarative option, but the underlying mechanism is $subscribe().
The detached option controls subscription lifetime. By default, a subscription created inside a component’s setup() is automatically removed when the component unmounts. If you want the subscription to persist beyond the component’s lifetime, pass { detached: true } as the second argument .
store.$subscribe(callback, { detached: true })
This is useful for global plugins and persistence layers that should run for the entire application lifetime.
c. $onAction(): observing action invocations
$onAction() registers a callback that fires before every action is invoked. The callback receives an object with store, name, args, after, and onError.
store.$onAction(({ name, args, after, onError }) => {
console.log(`Action ${name} called with`, args)
after((result) => {
console.log(`Action ${name} completed with`, result)
})
onError((error) => {
console.error(`Action ${name} failed:`, error)
})
})
The after callback fires when the action completes. If the action is async, after receives the resolved value, not the Promise . The onError callback fires when the action throws or rejects. This gives you full lifecycle instrumentation: you know when an action starts, what it received, when it finished, and whether it succeeded.
Like $subscribe(), $onAction() returns a function that removes the subscription. And like $subscribe(), it is automatically cleaned up when the component unmounts unless detached: true is passed .
The known limitation with Setup Stores is that $onAction() does not fire when one action in the same store calls another action directly. In a Setup Store, functionA and theMotherAction are both plain functions. When theMotherAction calls functionA(data), that is a local function call, not a property access on the store. The $onAction() wrapper only intercepts calls made through the store object . Option Stores do not have this limitation because actions are properties on the store object, and this.functionA() goes through the proxy.
The workaround is to call the action through the store instance: useMyStore().functionA(data). This is awkward and defeats some of the purpose, but it is the only way to trigger $onAction() for intra-store calls in Setup Stores .
Complete Example Session
The following session builds a store with $reset(), $subscribe(), and $onAction(), demonstrating the correct patterns for both Option and Setup syntaxes.
// ============================================
// PART 1: OPTION STORE WITH BUILT-IN RESET
// ============================================
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
coupon: null
}),
actions: {
add(item) {
this.items.push(item)
},
applyCoupon(code) {
this.coupon = code
}
}
})
// ============================================
// PART 2: SETUP STORE WITH MANUAL RESET
// ============================================
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const name = ref('Alice')
const age = ref(25)
function $reset() {
name.value = 'Alice'
age.value = 25
}
function updateName(newName) {
name.value = newName
}
return { name, age, $reset, updateName }
})
// ============================================
// PART 3: USING $RESET
// ============================================
const cart = useCartStore()
cart.add({ id: 1, name: 'Book' })
console.log(cart.items.length) // 1
cart.$reset()
console.log(cart.items.length) // 0 (built-in)
const user = useUserStore()
user.updateName('Bob')
user.$reset()
console.log(user.name) // 'Alice' (manual)
// ============================================
// PART 4: $SUBSCRIBE FOR PERSISTENCE
// ============================================
cart.$subscribe((mutation, state) => {
console.log('Mutation type:', mutation.type)
localStorage.setItem('cart', JSON.stringify(state))
})
cart.add({ id: 2, name: 'Pen' })
// Subscription fires, state saved to localStorage
// ============================================
// PART 5: DETACHED SUBSCRIPTION
// ============================================
cart.$subscribe((mutation, state) => {
console.log('Detached subscription fired')
}, { detached: true })
// This subscription survives component unmount
// ============================================
// PART 6: $ONACTION WITH AFTER HOOK
// ============================================
cart.$onAction(({ name, args, after, onError }) => {
console.log(`Action "${name}" called with`, args)
after((result) => {
console.log(`Action "${name}" finished`)
})
onError((error) => {
console.error(`Action "${name}" failed:`, error)
})
})
cart.add({ id: 3, name: 'Notebook' })
// Logs: Action "add" called with [{ id: 3, ... }]
// Logs: Action "add" finished
// ============================================
// PART 7: REMOVING A SUBSCRIPTION
// ============================================
const unsubscribe = cart.$subscribe((mutation, state) => {
console.log('This will be removed')
})
unsubscribe() // subscription removed
// ============================================
// PART 8: $ONACTION WITH ERROR HANDLING
// ============================================
cart.$onAction(({ name, onError }) => {
onError((error) => {
console.error(`Caught error in ${name}:`, error.message)
})
})
// If an action throws, onError fires
The eight parts cover the Option Store with built-in reset, the Setup Store with manual reset, using both reset methods, $subscribe() for persistence, detached subscriptions, $onAction() with the after hook, removing subscriptions, and $onAction() error handling. What the session demonstrates is that $reset() is syntax-dependent, while $subscribe() and $onAction() work identically in both syntaxes—with the caveat about intra-store action calls in Setup Stores.
Quick Reference
The Three Methods
| Method | Purpose | Returns |
|---|---|---|
$reset() | Restore initial state | void |
$subscribe(cb, opts?) | Observe state changes | Unsubscribe function |
$onAction(cb, detached?) | Observe action calls | Unsubscribe function |
$reset() Availability
| Store Type | $reset() |
|---|---|
| Option Store | ✅ Built-in |
| Setup Store | ❌ Throws error; manual or plugin required |
$subscribe() Mutation Types
| Type | Trigger |
|---|---|
'direct' | store.count = 1 |
'patch object' | store.$patch({ count: 1 }) |
'patch function' | store.$patch((s) => { s.count = 1 }) |
$onAction() Callback Arguments
| Property | Meaning |
|---|---|
name | Action name |
args | Arguments passed to action |
after(fn) | Callback on success |
onError(fn) | Callback on failure |
store | The store instance |
Detached Option
| Option | Effect |
|---|---|
detached: false (default) | Auto-removed on component unmount |
detached: true | Survives component unmount |
Best Practices
✅ Do This:
cart.$reset() // ✅ Option Store reset
function $reset() { count.value = 0 } // ✅ manual Setup reset
store.$subscribe(cb, { detached: true }) // ✅ global persistence
store.$onAction(({ after }) => after(() => log('done'))) // ✅ completion hook
❌ Don’t Do This:
setupStore.$reset() // ❌ throws without manual impl
// forget to return $reset from setup store // ❌ undefined
store.$subscribe(cb) // in a component that unmounts // ❌ subscription dies
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
$reset() throws on Setup Store | Not built-in | Implement manually or use plugin |
| Subscription stops working | Component unmounted | Use { detached: true } |
$onAction not firing in Setup Store | Intra-store action call bypasses proxy | Call via useStore().action() |
$reset not available after plugin | Testing without app instance | Create app in test setup |
| Persistence not working | Subscription created after state change | Create subscription before mutations |
Real-World Examples
1. Option Store reset
store.$reset()
2. Setup Store manual reset
function $reset() { count.value = 0; name.value = 'x' }
3. Persist cart to localStorage
store.$subscribe((_, state) => localStorage.setItem('cart', JSON.stringify(state)))
4. Detached persistence
store.$subscribe(cb, { detached: true })
5. Log all state mutations
store.$subscribe((mutation) => console.log(mutation.type))
6. Loading indicator via onAction
store.$onAction(({ name, after }) => {
loading.value = true
after(() => { loading.value = false })
})
7. Error tracking via onError
store.$onAction(({ onError }) => onError((e) => log(e)))
8. Audit log
store.$onAction(({ name, args }) => audit(`${name} called`, args))
9. Plugin-level subscription
pinia.use(({ store }) => {
store.$subscribe(cb, { detached: true })
})
10. Remove subscription
const off = store.$subscribe(cb)
off()
Visual
Option vs Setup Reset
┌──────────────────────────────────────────────┐
│ OPTION STORE │
│ state: () => ({ count: 0 }) │
│ store.count = 10 │
│ store.$reset() │
│ → count = 0 ✅ (built-in) │
│ │
│ SETUP STORE │
│ const count = ref(0) │
│ function $reset() { count.value = 0 } │
│ return { count, $reset } │
│ store.$reset() │
│ → count = 0 ✅ (manual) │
│ │
│ Setup without $reset: │
│ store.$reset() │
│ → ❌ throws error │
└──────────────────────────────────────────────┘
$subscribe Flow
┌──────────────────────────────────────────────┐
│ store.count = 1 │
│ │ │
│ ▼ │
│ Mutation recorded │
│ type: 'direct' │
│ │ │
│ ▼ │
│ $subscribe callbacks fire │
│ (mutation, state) │
│ │ │
│ ▼ │
│ Persistence logic runs │
│ (localStorage.setItem) │
└──────────────────────────────────────────────┘
$onAction Lifecycle
┌──────────────────────────────────────────────┐
│ store.add(item) called │
│ │ │
│ ▼ │
│ $onAction callback fires │
│ name: 'add' │
│ args: [item] │
│ │ │
│ ▼ │
│ Action executes │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ success error │
│ │ │ │
│ ▼ ▼ │
│ after() onError() │
└──────────────────────────────────────────────┘
Detached Subscription Lifetime
┌──────────────────────────────────────────────┐
│ COMPONENT MOUNTS │
│ store.$subscribe(cb) │
│ │ │
│ ▼ │
│ COMPONENT UNMOUNTS │
│ │ │
│ ▼ │
│ subscription removed ❌ (not detached) │
│ │
│ COMPONENT MOUNTS │
│ store.$subscribe(cb, { detached: true }) │
│ │ │
│ ▼ │
│ COMPONENT UNMOUNTS │
│ │ │
│ ▼ │
│ subscription persists ✅ (detached) │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
$reset() | Restore initial state |
| Option Store reset | Built-in |
| Setup Store reset | Manual or plugin |
$subscribe() | Observe state changes |
| Subscription cleanup | On component unmount |
detached: true | Survives unmount |
$onAction() | Observe action calls |
after hook | Action completed |
onError hook | Action failed |
Setup Store $onAction limitation | Intra-store calls not intercepted |
Key takeaways:
$reset()is not universal. Option Stores have it built-in. Setup Stores throw an error unless you implement it manually or use a plugin .$subscribe()fires on every state change. The callback receives the mutation type and the current state, making it the foundation for persistence .$onAction()fires before every action. TheafterandonErrorcallbacks give you completion and failure hooks .- Subscriptions are auto-cleaned on unmount. Pass
{ detached: true }to keep them alive beyond the component’s lifetime . - Setup Stores have an
$onActionlimitation. Actions calling other actions in the same store bypass the observer because they are plain function calls, not proxy property accesses . - Persistence uses subscribe-to-write, initialize-to-read. The state is saved on every mutation and restored from storage at store creation .
Remember: $reset(), $subscribe(), and $onAction() are the three lifecycle methods of a Pinia store. $reset() restores state but is not available on Setup Stores without manual implementation. $subscribe() observes state changes and is the mechanism behind every persistence plugin. $onAction() observes action invocations and provides completion and error hooks. Both observers are automatically cleaned up when the component unmounts unless you explicitly detach them. The Setup Store $onAction limitation is a sharp edge—if you need reliable action instrumentation across intra-store calls, Option Stores or explicit store-instance calls are the workaround.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!