In a Pinia order store, how do you write a synchronous addItem action and an asynchronous placeOrder action, and how does a checkout component call them?
answer
- methods of the store
- no commit, no dispatch
- this in option stores, no arrows
- async action returns a promise
- caller awaits and catches
basics
~20 sActions are functions: listed under actions in an option store, where they use this and so cannot be arrows, or returned from a setup store. An async action returns a promise the component awaits; failures reach the caller unless handled.
solid answer
~40 sIn an option store, `actions: { addItem(item) { this.items.push(item) }, async placeOrder() { … } }` defines both; `this` is the store, so actions must be regular functions, not arrow functions. In a setup store, every returned function is an action and uses closures instead of `this`. Components call them like methods, `order.addItem(item)` and `await order.placeOrder()`, with no `commit` or `dispatch`; arguments and return values are typed by inference. An async action can await an API call or other actions and returns a promise. If it throws or rejects, the caller receives the error, so either the action records an `error` and resolves, or the component catches it. Nothing is rolled back: state written before the failure stays written.
code
ts · 30 linesimport { defineStore } from 'pinia'
import { api } from '@/api'
interface CartItem { sku: string; qty: number }
export const useOrderStore = defineStore('order', {
state: () => ({
items: [] as CartItem[],
status: 'idle' as 'idle' | 'submitting' | 'placed' | 'failed',
lastOrderId: null as string | null,
}),
actions: {
addItem(item: CartItem) {
this.items.push(item)
},
async placeOrder() {
this.status = 'submitting'
try {
const { id } = await api.placeOrder(this.items)
this.lastOrderId = id
this.items = []
this.status = 'placed'
return id
} catch (error) {
this.status = 'failed'
throw error
}
},
},
})go deeper
Know that actions are methods under actions or functions in a setup store, can be async, and are called directly: order.placeOrder().
Explain why option-store actions cannot be arrows, how async actions return promises, and why failed actions leave partial writes.
Choose one error-handling contract per store, guard against double submission, and reset status in the failure path.
Set conventions for where API calls and error state live, store actions versus components, so checkout logic is not split across layers.
## Actions are the store's methods Pinia's docs describe **actions** as the equivalent of component methods, and the place for business logic. There is no separate mutation layer: an action changes state directly, calls APIs, calls other actions and returns whatever it likes. | Store style | How an action is declared | How it reaches state | |---|---|---| | Option store | a method under `actions` | `this.items`, `this.status` | | Setup store | a function returned from the setup function | closures over refs: `items.value` | ## Option-store actions and this In an option store, Pinia calls each action with the store as `this`, fully typed, including state, getters and other actions. That is why actions are written as **regular functions**: an arrow function takes `this` from its surrounding scope and never sees the store. The docs' own example carries the comment: since we rely on `this`, we cannot use an arrow function. Pinia also binds actions for you: calling a destructured action, `const { addItem } = order; addItem(item)`, still runs with the store as `this`, so actions (unlike state) can be destructured safely. ## Async actions Unlike getters, actions can be `async`. A typical checkout action follows four steps: 1. set a status: `this.status = 'submitting'`; 2. await the API: `const { id } = await api.placeOrder(this.items)`; 3. write the result: `this.lastOrderId = id; this.items = []; this.status = 'placed'`; 4. on failure, record it: `this.status = 'failed'; this.error = message`. The action returns a **promise**. Whatever the function returns becomes the resolved value, so `placeOrder` can return the new order id to the component that needs to navigate to a confirmation page. ## Errors and partial writes - A thrown error or a rejected promise **propagates to the caller**. If no one catches it, it becomes an unhandled rejection. - Pinia does **not** roll back state when an action fails. `status` stays `'submitting'` unless the action's own `catch` resets it. - Two common designs: the action catches, stores `error`, and resolves (the component just reads `order.error`); or the action rethrows and the component shows the failure. Pick one per store and be consistent. ## Calling actions from a component - `order.addItem(item)`: a plain call, synchronous. - `await order.placeOrder()` inside an `async` handler, wrapped in `try`/`catch` when the action rethrows. - In a template: `@click="order.addItem(item)"`. - Disable the submit button while `order.status === 'submitting'`, so a double click does not place two orders. There is no `order.dispatch('placeOrder')` or `commit`: those belong to Vuex, which Pinia replaced. ## Setup-store actions In a setup store the same actions are plain functions: ```ts function addItem(item: CartItem) { items.value.push(item) } async function placeOrder() { status.value = 'submitting' const { id } = await api.placeOrder(items.value) status.value = 'placed' return id } return { items, status, addItem, placeOrder } ``` There is no `this`; the functions close over the refs. Pinia wraps each returned function as an action, so it behaves the same for callers and for action hooks. ## Slips interviewers listen for - Writing option-store actions as arrow functions. - Reaching for `dispatch` or `commit` out of Vuex habit. - Calling an async action without awaiting or catching it: a rejection then surfaces as an unhandled promise rejection. - Assuming a failed action restores the state it changed. ## Arguments, return values and typing Actions take any arguments and return anything, and Pinia infers both for callers: `order.addItem` is typed from its parameter, and `await order.placeOrder()` is typed as whatever the action returns, here the order id. In an option store, `this` inside an action is typed as the whole store, including state, getters and the other actions, so annotations are rarely needed; unlike `this`-based getters, actions normally get their return types inferred. Actions can also call each other: `this.addItem(item)` in an option store, or a plain call in a setup store. An async action can `await` another action as easily as an API call, which is how larger flows such as validate-then-submit are composed out of small actions. ## When logic belongs in an action 1. It changes the store's state, possibly after asynchronous work. 2. More than one component needs it, such as the cart page and the mini-cart both adding items. 3. It needs to be observable or testable in isolation from the UI. Logic that only formats data for one component stays in that component; logic that only derives values from state belongs in a getter.
- Why can a Pinia action be destructured from the store while state cannot?Pinia wraps every action so that, when it is called without the store as `this`, it applies the store itself. `const { addItem } = order` therefore still works. State properties are plain values once destructured, copied out of the reactive store, so they stop updating; state needs `storeToRefs()` instead.
- Should a Pinia placeOrder action catch its own errors or rethrow them?Either works if it is consistent. Catching and storing `error` keeps components simple: they render `order.error`. Rethrowing after recording the status lets the caller decide, for example to stay on the page instead of navigating. What must not happen is neither: a rejection nobody awaits becomes an unhandled promise rejection.
saying these in an interview costs you the question
- Pinia actions change state through commit, like Vuex mutations.
- Option-store actions are best written as arrow functions.
- Actions must stay synchronous; API calls belong in components.
- Pinia rolls back state changes when an action throws.
- A destructured action loses its this and breaks.