skip to content

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%

answer

  1. ref, computed, function
  2. returned computed is a getter
  3. no this, closures over refs
  4. setters are possible
  5. a function is an action

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.

solid answer

~40 s

A setup store's returned values are sorted by kind: a `ref()` or `reactive()` becomes state, a `computed()` becomes a getter, a function becomes an action. So `const total = computed(() => subtotal.value + tax.value)`, returned, is a getter: read as `invoice.total`, cached, recomputed lazily, never in `$state`. Differences from option getters: no `state` argument or `this`, just closures over refs and other computeds, so types are inferred with no return annotations; other stores are read from a `useX()` call at the top of setup; and a writable `computed({ get, set })` works, so `invoice.total = …` calls the setter, which option getters cannot offer. Two mistakes to spot: returning a plain function instead of a computed makes it an uncached action, and computing a value into a `ref()` freezes a snapshot into state.

code

ts · 18 lines
ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useInvoiceStore = defineStore('invoice', () => {
  const net = ref(1000)
  const taxRate = ref(0.2)

  // getter: cached, not in $state
  const tax = computed(() => net.value * taxRate.value)

  // writable getter: invoice.gross = 1200 sets net to 1000
  const gross = computed({
    get: () => net.value + tax.value,
    set: (value: number) => { net.value = value / (1 + taxRate.value) },
  })

  return { net, taxRate, tax, gross }
})

go deeper

for a junior

Know that in a setup store refs become state, computeds become getters and functions become actions.

for a middle

Compare setup computeds with option getters: closures instead of this, inferred types, writable computed, where other stores are read.

for a senior

Catch kind-changing slips: functions returned instead of computeds, values frozen into refs, and computeds never returned.

for a principal

Weigh option versus setup stores for a codebase, trading explicit sections for inference, composables and writable getters.

## How Pinia sorts a setup store's return value A **setup store** is `defineStore('invoice', () => { …; return { … } })`. Pinia runs the function once and classifies each returned property: | Returned value | Becomes | In `$state`? | |---|---|---| | `ref()`, `reactive()` | state | yes | | `computed()` | getter | no | | function | action | no | Pinia recognises a computed as a ref that carries its own reactive effect, which is exactly what `computed()` returns. So a getter is simply a returned `computed`: ```ts export const useInvoiceStore = defineStore('invoice', () => { const lines = ref<Line[]>([]) const taxRate = ref(0.2) const subtotal = computed(() => lines.value.reduce((s, l) => s + l.qty * l.unitPrice, 0)) const total = computed(() => subtotal.value * (1 + taxRate.value)) return { lines, taxRate, subtotal, total } }) ``` It behaves like an option-store getter where it counts: read as `invoice.total`, **cached** until the refs it read change, computed **lazily** on the next read, shared by every component. ## Setup computed versus option getter | | Option-store getter | Setup-store `computed` | |---|---|---| | Reads state through | the `state` argument or `this` | closures over refs | | Reads other getters through | `this`, with an annotated return type | the other computed's `.value` | | TypeScript | return type needed for `this`-based getters | inferred | | Reads another store | `useX()` inside the getter body | `useX()` at the top of setup | | Writable | no, read-only | yes, with `computed({ get, set })` | The typing row is a common reason teams prefer setup stores: without `this`, there is no circular inference and no return types to write. ## Writable getters Because a setup store returns real `computed` refs, a **writable computed** works as a writable getter. With `const grossTotal = computed({ get: () => …, set: (v) => { … } })` returned, `invoice.grossTotal = 1200` calls the setter, which can adjust the underlying state (for example, back-calculate a discount). The value is still not state: it is not in `$state`, and the setter is the only way it changes. An option-store getter has no such form: it is a plain function, and assigning to it is refused with a development warning. ## Mistakes that change the kind The classification is by value, so small slips change what Pinia builds: 1. **Returning a function instead of a computed.** `function total() { return … }` becomes an **action**: callers write `invoice.total()`, it recomputes on every call, and action hooks see every call. 2. **Computing into a ref.** `const total = ref(sum(lines.value))` runs `sum` once, at setup, and returns a ref, so Pinia treats it as **state** holding a snapshot that never follows the lines. 3. **Not returning it.** A computed kept private is not on the store: components cannot read it, and devtools does not list it. 4. **Reading another store in the setup body.** Read it inside the computed, so the dependency is tracked and mutual store use stays legal. ## When the choice matters - Option stores make the three kinds explicit with `state`, `getters` and `actions` keys; setup stores rely on what you return. - Setup stores can use composables and writable computeds, and need no getter type annotations. - Either way, a getter is the right home for any value derived from state; the style only changes how you write it. ## Reading a setup-store getter Components cannot tell the two styles apart: - `invoice.total` reads the getter from a template or `<script setup>`, with no `.value`, because the store unwraps refs; - inside the setup function itself, the same getter is `total.value`, because there it is still a `computed` ref; - `storeToRefs(invoice)` returns it as a ref again for destructuring. That `.value` asymmetry is a frequent source of small bugs when code moves between the store body and its callers: forgetting `.value` inside the store compares a ref object, not a number, and forgetting that the store unwraps leads to `invoice.total.value`, which is `undefined`.

  • Is a writable computed returned from a Pinia setup store part of $state?
    No. Pinia classifies any computed as a getter, writable or not, so it is left out of `$state`, devtools' state and serialisation. Assigning to it on the store runs the setter, which changes the real state refs; only those refs are stored and serialised.
  • Why do setup-store getters need no return type annotations?
    They never read the store through `this`. Each computed closes over refs and other computeds whose types are already known, so TypeScript infers the result directly. The circular inference that forces annotations on `this`-based option getters simply does not arise.

saying these in an interview costs you the question

  • A computed returned from a setup store is serialised with the state.
  • Returning a plain function from a setup store gives a cached getter.
  • Setup-store getters need explicit return types, like this-based getters.
  • Getters in setup stores can never be written to.
  • const total = ref(sum(lines.value)) keeps total in sync with lines.