Learn Pinia - Actions & Async Logic
Episode 7 of 23

Learn Pinia - Actions & Async Logic

An action is where mutation logic and side effects live in Pinia. This episode covers basic actions, async actions that call APIs with loading and error status, and actions that call other stores. You also learn reusability patterns so an action can be called from any component.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

State stores data, getters compute derived values, and actions bring data into the store. An action is a method that may change state, call APIs, set timers, or coordinate with other stores — all side effects live here, not in components.

Episode 7 covers actions from the basics to production async patterns: simple synchronous actions, async actions with loading and error status, and actions that call other stores. By the end of the episode, you'll have an action pattern that any component can use without duplicating logic.

Simple Action

A basic action is a method that changes state. In an Options store, use this; in a Setup store, use a closure:

JSAction in an Options store
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as { id: number; name: string; qty: number }[],
  }),
  actions: {
    addItem(item: { id: number; name: string }) {
      const existing = this.items.find((i) => i.id === item.id)
      if (existing) {
        existing.qty++
      } else {
        this.items.push({ ...item, qty: 1 })
      }
    },
    clear() {
      this.items = []
    },
  },
})

addItem(item) applies the business logic (if it exists, bump the qty; if not, push a new one) in a single place. Components just call store.addItem(item) — no logic duplicated in every button.

Async Action and Loading Status

When an action calls an API, store the loading and error status in state so components can render accordingly:

JSAsync action with status
export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null as { name: string } | null,
    loading: false,
    error: null as string | null,
  }),
  actions: {
    async fetchProfile() {
      this.loading = true
      this.error = null
      try {
        const res = await fetch('/api/profile')
        if (!res.ok) throw new Error('Failed to load profile')
        this.profile = await res.json()
      } catch (e) {
        this.error = e instanceof Error ? e.message : 'Something went wrong'
      } finally {
        this.loading = false
      }
    },
  },
})

The loading, error, then finally pattern is the standard for all async actions. this.loading = true at the start and this.loading = false in finally guarantee the status is always correct, including when an error occurs.

Calling Another Store in an Action

Actions may coordinate with other stores through useXStore():

JSAction calling another store
import { useAuthStore } from '@/stores/auth'
 
export const useCheckoutStore = defineStore('checkout', {
  state: () => ({ status: 'idle' as 'idle' | 'success' | 'error' }),
  actions: {
    async checkout() {
      const authStore = useAuthStore()
      if (!authStore.isLoggedIn) {
        await authStore.redirectToLogin()
        return
      }
      this.status = 'success'
    },
  },
})

useAuthStore() inside the action makes sure the auth store is activated when needed. This is the core pattern for composing stores, which we'll cover more deeply in episode 12.

Using an Async Action in a Component

Async actions return a Promise, so components can await the result and handle success or failure:

JSCall an action from a component
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
 
const store = useUserStore()
 
async function muat() {
  await store.fetchProfile()
  if (store.error) {
    console.error(store.error)
  }
}
</script>

await store.fetchProfile() makes the component wait until the action finishes. The action also serves as the source of truth for status, so the UI can show a spinner when loading is true and an error message when error is set.

Warning

Don't call APIs directly in components and then write the results into the store with store.$patch. Put the entire flow inside an action — this makes testing easier (episode 17) and avoids scattered logic.

Closing

Episode 7 closes out Pinia's basic triangle with actions. You can now write synchronous and async actions, manage loading and error status, call other stores from inside an action, and use async actions from components with await.

Key takeaways:

  • Actions are where state mutations and side effects live.
  • Async actions store loading and error in state.
  • Always reset the error and set loading to true at the start of an action.
  • finally guarantees loading is turned off on every path.
  • Actions can call other stores with useXStore().
  • Call an action from a component with await to follow its result.

In the next episode, episode 8, we'll discuss Pinia's plugin system — how to add global properties to all stores, accept plugin options, and use $subscribe and $onAction for observability. This paves the way for features like persistence in episode 9.