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?
answer
- the store's computed values
- getters option of defineStore
- read as a property
- one cache per store
- recomputes lazily after a change
basics
~20 sAdd 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.
solid answer
~40 sIn an option store, `getters: { subtotal: (state) => …, total(): number { return this.subtotal + this.tax } }` defines derived values. Pinia wraps each one in a Vue `computed()` owned by the store, so `invoice.total` is read like a property, in a template or in `<script setup>`, never called. The value is cached: computed on the first read, reused by every component that reads it, and recalculated lazily, on the next read after the state it depends on (the line items, the tax rate) changes. A getter is not state: it is absent from `$state` and not serialised, and in an option store it is read-only, so `invoice.total = 0` only produces a development warning. To destructure it and stay reactive, use `storeToRefs()`.
code
vue · 12 lines<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useInvoiceStore } from '@/stores/invoice'
const invoice = useInvoiceStore()
const { total } = storeToRefs(invoice) // stays reactive
</script>
<template>
<p>Subtotal: {{ invoice.subtotal }}</p>
<p>Total: {{ total }}</p>
</template>go deeper
Know that getters are declared under getters, read as properties like invoice.total, and cached until the state they use changes.
Explain that each getter is a computed owned by the store: lazy, shared by all readers, absent from $state and read-only in option stores.
Use getters for every value derived from state so nothing drifts, keep them side-effect free, and spot totals duplicated into state by hand.
Set the convention that derived data lives in getters, not in state or scattered component computeds, so a store stays the single source of truth.
## Getters are the store's computed values A **getter** is a value derived from a store's state. Pinia's docs describe getters as exactly the equivalent of Vue's `computed` values, for a store. In an **option store** they are declared under the `getters` key of `defineStore()`: ```ts export const useInvoiceStore = defineStore('invoice', { state: () => ({ lines: [] as { qty: number; unitPrice: number }[], taxRate: 0.2, }), getters: { subtotal: (state) => state.lines.reduce((sum, l) => sum + l.qty * l.unitPrice, 0), tax(): number { return this.subtotal * this.taxRate }, total(): number { return this.subtotal + this.tax }, }, }) ``` ## Reading a getter A getter appears on the store as a **property**, next to the state: - in a template: `{{ invoice.total }}`; - in `<script setup>`: `invoice.total`; - never as a call: `invoice.total()` fails, because the value is a number, not a function; - there is no separate `getters` object, as there was in Vuex: the getter sits directly on the store. Destructuring `const { total } = invoice` copies the current number and stops updating; `storeToRefs(invoice)` returns refs for state and getters that stay reactive. ## How the cache works When the store is created, Pinia wraps every getter in a `computed()` that belongs to the store. The computed behaves like any Vue computed: 1. The **first read** runs the getter and records which reactive values it read (`lines`, `taxRate`, and other getters it used). 2. **Later reads** return the cached value without running the getter again. 3. When one of those recorded values **changes**, the computed is marked stale; nothing is recomputed yet. 4. The **next read** after that runs the getter once and caches the new value. 5. A getter nobody reads after a change is **not** recomputed at all. ## One cache per store, shared by every component The computed belongs to the store, not to a component. With three components showing the invoice total, one change to a line item costs **one** recomputation, on the first read, and all three read the same cached value. | Where the total lives | Computations after one change | Shared? | |---|---|---| | Pinia getter `total` | one, on next read | yes, by every component | | a `computed()` inside each component | one per component | no | | a `total` field in state, updated by hand | whenever code remembers to update it | yes, but can drift | The third row is the classic mistake: a total stored in state must be kept in sync by every action that touches the lines, and sooner or later one forgets. A getter cannot drift. ## What a getter is not - **Not state.** It is absent from `invoice.$state`, from devtools' state view and from the state that is serialised for server rendering. It is recomputed from state instead. - **Not writable, in an option store.** `invoice.total = 0` does nothing; a development build warns `Set operation on key "total" failed: target is readonly.` TypeScript marks option-store getters `readonly`. - **Not allowed to share a name with a state property.** A development build reports diagnostic `PINIA_R1002` when a getter and a state property have the same name. - **Not parameterised.** A getter cannot take arguments; a getter that returns a function is the workaround, and it gives up caching for the calls. ## When to use one 1. Any value derived only from state, such as totals, counts, filtered lists and flags like `isOverdue`. 2. Values several components need: the cache is shared. 3. Values that must never drift from the data they summarise. Keep getters free of side effects: no requests, no writes to state. That work belongs in actions. ## Getters used inside the store Getters are not only for components. Inside the store they are read the same way: - an option-store action reads `this.total`, for example to refuse sending an invoice whose total is zero; - one getter builds on another, as `total` builds on `subtotal` and `tax`, and each keeps its own cache, so `subtotal` is not recomputed just because `total` was read twice; - another store can read `useInvoiceStore().total` from its own getters or actions. ## Interview slips to avoid - Saying a getter "runs on every render": it runs only when stale and read. - Saying each component gets its own copy: there is one computed per store. - Trying to write to a getter to "override" a total: change the state it derives from instead, or keep an explicit override field in state that the getter consults. - Calling the getter, `invoice.total()`, out of Vuex or method habit.
- Why is an invoice total better as a Pinia getter than as a field updated by actions?A stored total has to be recalculated by every action that touches lines, discounts or the tax rate; one forgotten update and the UI shows a wrong number. A getter is derived from the state on read, so it cannot drift, and its cache means the cost is one calculation per change, shared by every reader.
- What happens if a Pinia option store has a state property and a getter with the same name?They collide on the store object, which exposes both state and getters as properties. A development build reports diagnostic `PINIA_R1002`, saying a getter cannot share a name with a state property. Rename one of them, for example `total` as the getter and `totalOverride` in state.
A getter is like the total cell of a shared spreadsheet: when a cell it references changes it is only marked stale, it is recalculated the next time anyone looks, and everyone who looks sees the same cell.
saying these in an interview costs you the question
- A Pinia getter is called like a method: invoice.total().
- Getters live under store.getters, as they did in Vuex.
- Every component that reads a getter computes its own copy.
- A getter recomputes on every read, like a plain function.
- Assigning a value to an option-store getter overrides it until the next change.