skip to content

Pinia

Pinia is Vue's official store: option or setup stores with state, cached getters and actions, no mutations, plus plugins and SSR hydration. Interviewers compare it with Vuex.

on this pageshow

explore

questions

page 1 of 2

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?

level: juniorimportance: must knowfreq 68%

answer

  1. methods of the store
  2. no commit, no dispatch
  3. this in option stores, no arrows
  4. async action returns a promise
  5. caller awaits and catches

basics

~20 s

Actions 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 s

In 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 lines
ts
import { 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

for a junior

Know that actions are methods under actions or functions in a setup store, can be async, and are called directly: order.placeOrder().

for a middle

Explain why option-store actions cannot be arrows, how async actions return promises, and why failed actions leave partial writes.

for a senior

Choose one error-handling contract per store, guard against double submission, and reset status in the failure path.

for a principal

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.
open as a page

In a Vue 3 app with Pinia 4, what do createPinia(), app.use(pinia) and defineStore('cart', …) each do before a header badge can call useCartStore()?

level: juniorimportance: must knowfreq 72%

basics

~20 s

createPinia() builds the root container, app.use(pinia) installs it so components can find it, and defineStore('cart', …) only returns a useCartStore function; the store is created on its first call and then shared by every component.

open as a page

In a Pinia option store for invoices, how do you define a total-with-tax getter, how do components read it, and when does it recompute?

level: juniorimportance: must knowfreq 64%

basics

~20 s

Add it under getters, for example subtotal: (state) => …, and read invoice.total as a property. Pinia wraps each getter in one computed per store, so all components share a cached value that recomputes only after state it read changes.

open as a page

In a Pinia store for a user-preferences panel, when do you write state directly, call $patch with an object, or call $patch with a function?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Direct writes suit single fields and v-model. $patch with an object applies several fields at once, merging nested objects but replacing arrays. $patch with a function mutates state in place for array edits or logic; both patch forms notify subscribers once.

open as a page

How do you unit-test a Pinia notifications store's actions and getters, and why call setActivePinia(createPinia()) before each test?

level: juniorimportance: must knowfreq 50%

basics

~20 s

Call setActivePinia(createPinia()) in beforeEach, then call useNotificationsStore() directly and exercise real actions and getters. A fresh pinia per test gives a fresh store, because stores are cached per pinia and state would otherwise leak between tests.

open as a page

In a Vue 3 app, what are the main differences between Pinia and Vuex 4, and why is Pinia now the recommended store?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Pinia drops Vuex's mutations, replaces one store of nested namespaced modules with flat stores named by id, and infers types from the store definition instead of string keys. Vuex is in maintenance mode; Pinia is the official recommendation.

open as a page

In Pinia, how does an order store's placeOrder action use the auth store's token, and why call useAuthStore() before the first await?

level: middleimportance: must knowfreq 50%

basics

~20 s

Call useAuthStore() inside the action, or at the top of a setup store, then read auth.token or call its actions. Do it before any await: after one, another request's pinia may be the active one.

open as a page

In Pinia, a header badge destructures `const { items, itemCount, addItem } = useCartStore()`; which of the three break, and how does storeToRefs fix it?

level: middleimportance: must knowfreq 77%

basics

~20 s

Destructuring a Pinia store copies current values: itemCount freezes, and items keeps the old array once the store replaces it. addItem works because actions stay bound to the store. storeToRefs(store) returns linked refs for state and getters, skipping actions.

open as a page

In Pinia, what does a store plugin receive in its context, and how would you use it to audit-log every action of every store?

level: middleimportance: must knowfreq 46%

basics

~20 s

A Pinia plugin receives one context object with pinia, app, store and options; an audit plugin calls store.$onAction on each store and logs the action name, arguments and outcome from the after and onError hooks.

open as a page

In a Pinia single-page app, why does calling useAuthStore() at the top of the router module fail, and where should the call go?

level: middleimportance: must knowfreq 60%

basics

~10 s

A store call needs a pinia; module-level code runs at import, before app.use(pinia) makes one active, so Pinia throws a no-active-Pinia error in development. Call useAuthStore() inside the guard function, which runs after installation.

open as a page

In Pinia, why does calling $reset() on a setup store fail, and how do you give a preferences store a working reset to defaults?

level: middleimportance: must knowfreq 58%

basics

~20 s

$reset() re-runs an option store's state() and patches the fresh values in. A setup store has no state() to re-run, so its built-in $reset throws in development and does nothing in production; return your own $reset built on a defaults factory.

open as a page

When a toast component test mounts with createTestingPinia() from @pinia/testing, what happens to the notifications store's actions, and how do you assert on them?

level: middleimportance: must knowfreq 44%

basics

~10 s

By default createTestingPinia() replaces every action with a stub spy, so calls are recorded but the real code never runs. Get the store with useNotificationsStore() in the test and assert with expect(store.dismiss).toHaveBeenCalledWith(id).

open as a page

Why does Pinia have no mutations when Vuex routed every state change through commit, and what replaced them?

level: middleimportance: must knowfreq 52%

basics

~10 s

Vuex mutations existed so devtools could record each synchronous state change. Pinia's devtools integration records direct writes and $patch calls itself, so mutations were dropped; state now changes through actions, direct assignment or $patch.

open as a page

In Pinia, how do you register a store plugin with pinia.use(), and what happens to the object that plugin returns?

level: juniorimportance: should knowfreq 41%

basics

~20 s

You pass a function to pinia.use(); Pinia calls it once for every store created afterwards and merges the returned object's keys onto that store, where refs are unwrapped and, in development, devtools lists the keys as custom properties.

open as a page

In Nuxt 4, how do you add Pinia through @pinia/nuxt, and what SSR work does the module do so you do not write it?

level: juniorimportance: should knowfreq 36%

basics

~20 s

Install pinia and @pinia/nuxt and list '@pinia/nuxt' in modules. The module creates and installs a pinia per request, ships its state in Nuxt's payload, restores it on the client, and auto-imports defineStore, storeToRefs and your stores.

open as a page

In Pinia, what does store.$onAction() let you observe, and how do its after and onError hooks behave for an async placeOrder action?

level: middleimportance: should knowfreq 40%

basics

~20 s

$onAction registers a listener that runs before every action of the store, with its name, store and args plus after and onError. For an async action, after receives the resolved value and onError the rejection, which still reaches the caller.

open as a page

In Pinia, how does an option store differ from a setup store, and how does defineStore map a setup store's refs, computeds and functions?

level: middleimportance: should knowfreq 63%

basics

~20 s

An option store passes an object of state(), getters and actions; a setup store passes a function whose returned refs become state, computeds getters and functions actions. Setup stores allow watchers and almost any composable; only option stores get $reset().

open as a page

In a Pinia setup store, how does a computed() become a getter, and how does it differ from an option-store getter?

level: middleimportance: should knowfreq 36%

basics

~20 s

Pinia treats every computed() a setup store returns as a getter: cached, absent from $state. Unlike option getters it closes over refs instead of using state or this, infers its type, and can be writable when given a setter.

open as a page

In a Pinia option store, when does a getter use the state argument versus this, and why does a this-based getter need a return type?

level: middleimportance: should knowfreq 48%

basics

~20 s

Use (state) => … when a getter only reads state; its type is inferred. To read other getters, write a regular function and use this, the whole store. TypeScript cannot infer that circular type, so annotate the return type.

open as a page

In Pinia, what does a $subscribe callback receive for each kind of write, and how do you build a preferences audit log from it?

level: middleimportance: should knowfreq 44%

basics

~20 s

A $subscribe callback gets a mutation (type 'direct', 'patch object' or 'patch function', the storeId, and a payload only for object patches) plus the live state. An audit log keeps its own snapshot and diffs, because no old value is passed.

open as a page

In a Pinia option store, why must state() declare every key up front, and how do you type keys starting as null or []?

level: middleimportance: should knowfreq 36%

basics

~20 s

Pinia makes exactly the keys that state() returns into state, so an undeclared key never becomes state. Type empty starts with casts such as [] as string[] and null as Profile | null, or give state() an interface return type.

open as a page

When moving from Vuex to Pinia, how do nested namespaced modules become flat stores, and what replaces rootState and rootGetters?

level: middleimportance: should knowfreq 41%

basics

~20 s

Each Vuex module, nested or not, becomes its own Pinia store whose id keeps the old namespace, such as 'authUser' for 'auth/user'. rootState and rootGetters give way to importing the other store and calling its useStore function inside a getter or action.

open as a page

In Pinia, the order store's placeOrder calls the auth store and auth's logout clears the order store; how do two setup stores use each other safely?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Both setup stores may call each other's useStore() at the top and call each other's actions. Neither may read the other's state directly in its setup body, because one of them is still unfinished then; read inside actions and computeds.

open as a page

A Pinia setup store for a cart keeps a `couponCode` ref out of its return object to make it private; what breaks, and what should you do instead?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Pinia treats only returned refs and reactive objects as state, so an unreturned couponCode is missing from pinia.state: it is not serialised for SSR and is invisible to devtools, $subscribe and plugins. Return every state ref; put internals in a separate store.

open as a page

An invoice list calls the Pinia getter invoicesByStatus(status) in every row and filter chip; why is nothing cached, and how do you make these lookups cheap?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A Pinia getter cannot take parameters, so invoicesByStatus returns a function; the computed caches that function, not its results, and every call filters again. Precompute a grouped index in an ordinary getter and look up by key.

open as a page

In Pinia, how does an invoice store's getter read a settings store's tax-rate getter, and what keeps that cross-store getter cached and bound to the right pinia?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Call useSettingsStore() inside the getter, or at the top of a setup store, and read settings.taxRate. The computed tracks that read, so it recomputes when the rate changes, and Pinia activates the invoice store's own pinia before each getter runs.

open as a page

A Pinia plugin returns { hasError: ref(false) } for every store; why is hasError missing from SSR state and $reset, and how should it be added?

level: seniorimportance: should knowfreq 28%

basics

~20 s

A returned ref is only a store property: it never enters store.$state, so SSR does not serialise it and $reset ignores it. Add real state by writing a ref to store.$state when absent, then linking store.hasError with toRef.

open as a page

In a Pinia plugin that adds the Vue Router instance to every store, why wrap it in markRaw(), and what breaks without it?

level: seniorimportance: should knowfreq 33%

basics

~10 s

Every store is reactive(), so an external object put on it is read back through a reactive proxy that unwraps its refs; store.router.currentRoute.value then fails. markRaw() makes the store hand back the untouched router.

open as a page

How would you build a Pinia plugin that persists only stores declaring a persist option, such as a theme store and a form draft, and type that option?

level: seniorimportance: should knowfreq 37%

basics

~20 s

Stores opt in with a custom persist option, the third defineStore argument for setup stores; the plugin reads options.persist, restores saved state with $patch, saves on $subscribe, skips the server, and DefineStoreOptionsBase types the option.

open as a page

In a server-rendered storefront using Pinia, why must each request create its own pinia, and why is useAuthStore() after an await in a guard risky?

level: seniorimportance: should knowfreq 41%

basics

~20 s

A pinia caches every store, so one shared pinia serves one shopper's cart to the next; create it per request. After an await, a store call falls back to the process-wide active pinia, so pass the request's pinia explicitly.

open as a page

showing 1–30 of 41