Actions in Pinia are methods that modify state. Unlike Vuex, there's no distinction between mutations and actions—actions can directly modify state.
Basic Actions
Options Syntax
typescriptimport { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, history: [] as number[] }), actions: { // Simple action increment() { this.count++ }, // Action with parameters incrementBy(amount: number) { this.count += amount }, // Action with validation setCount(value: number) { if (value < 0) { console.warn('Count cannot be negative') return } this.history.push(this.count) this.count = value }, // Action using getters doubleAndIncrement() { this.count = this.doubleCount + 1 // this.doubleCount is a getter }, // Action calling other actions reset() { this.setCount(0) this.history = [] } } })
Setup Syntax
typescriptimport { ref } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const history = ref<number[]>([]) function increment() { count.value++ } function incrementBy(amount: number) { count.value += amount } function setCount(value: number) { if (value < 0) { console.warn('Count cannot be negative') return } history.value.push(count.value) count.value = value } function reset() { setCount(0) history.value = [] } return { count, history, increment, incrementBy, setCount, reset } })
Returning Values from Actions
typescriptactions: { // Return success/failure addItem(item: Item): boolean { if (this.items.length >= this.maxItems) { return false } this.items.push(item) return true }, // Return the created item createItem(data: ItemData): Item { const item = { id: Date.now(), ...data, createdAt: new Date() } this.items.push(item) return item } } // Usage const success = store.addItem(newItem) if (!success) { alert('Cart is full!') } const created = store.createItem({ name: 'New Item' }) console.log('Created:', created.id)
Actions Accessing Other Stores
typescriptimport { defineStore } from 'pinia' import { useUserStore } from './user' import { useNotificationStore } from './notification' export const useCartStore = defineStore('cart', { state: () => ({ items: [] as CartItem[] }), actions: { async checkout() { const userStore = useUserStore() const notificationStore = useNotificationStore() if (!userStore.isLoggedIn) { notificationStore.show('Please log in to checkout', 'error') return } try { await api.checkout(this.items, userStore.id) this.items = [] notificationStore.show('Order placed!', 'success') } catch (error) { notificationStore.show('Checkout failed', 'error') } } } })
Subscribing to Actions
Listen to actions being called:
typescriptconst store = useCartStore() const unsubscribe = store.$onAction(({ name, // Action name store, // Store instance args, // Arguments passed to action after, // Hook after action returns onError // Hook if action throws }) => { const startTime = Date.now() console.log(`Action "${name}" started with args:`, args) after((result) => { console.log( `Action "${name}" finished in ${Date.now() - startTime}ms`, 'Result:', result ) }) onError((error) => { console.error(`Action "${name}" failed:`, error) }) }) // Unsubscribe when needed unsubscribe() // Keep subscription after component unmounts store.$onAction(callback, true) // detached: true
Error Handling in Actions
typescriptimport { defineStore } from 'pinia' export const useDataStore = defineStore('data', { state: () => ({ data: null as Data | null, error: null as Error | null, loading: false }), actions: { async fetchData() { this.loading = true this.error = null try { const response = await fetch('/api/data') if (!response.ok) { throw new Error(`HTTP ${response.status}`) } this.data = await response.json() } catch (e) { this.error = e as Error throw e // Re-throw for caller to handle } finally { this.loading = false } } } })