State, Getters and Actions
The three parts of an option store: state is the data, a getter is a computed value over it, an action is a function that changes it.
defineStore('cart', { state: () => ({ /* data */ }), getters: {}, actions: {} })An option store is defineStore(id, options) where the options object has three keys. They line up with what you already write in a component: state is the data, getters are computed values, actions are the functions.
State
state is a function that returns the initial object, not the object itself. Each application gets its own copy from that function, which matters on a server where one process renders many requests. Read and write properties straight off the store: cart.coupon = 'SPRING'. To group several writes into one devtools entry, use store.$patch({ ... }), or store.$patch((state) => { ... }) when you need to push into an array.
Getters
A getter is a computed value over state. It is cached until something it read changes. Written as an arrow function it receives state as its first argument. Written as a normal function it can reach other getters through this, and TypeScript then needs an explicit return type. Getters take no arguments; to filter by an id, return a function from the getter, which gives up the caching.
Actions
An action is an ordinary function on the store, with this bound to the store. There is nothing special about it: it may be async, it may await a fetch, and it may call other actions. Pinia has no mutations, so an action assigns to state directly.
What trips people up
Only properties returned by state() exist. Declare everything up front, null included.
Example
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
function onClear() {
cart.clear()
}
</script>
<template>
<section>
<p>{{ cart.count }} items, {{ cart.formattedTotal }}</p>
<button type="button" :disabled="cart.isEmpty" @click="onClear">
Clear cart
</button>
</section>
</template>
<style scoped>
section {
display: grid;
gap: 0.5rem;
}
</style>When to use it
- Deriving a cart total and item count from the lines, in one place, instead of in every component that shows them.
- Writing an async action that posts a form and stores the saved record.
- Grouping a filter reset, which clears four fields at once, into a single $patch.
More examples
The store behind it
count and isEmpty are arrow getters, so they take state as an argument. totalCents and formattedTotal are normal functions because they read other getters through this, and each carries an explicit return type so vue-tsc can check them. coupon is declared as null with a type assertion, because a property missing from state() can never be added later.
// src/stores/cart.ts
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
lines: [{ id: 'a1', quantity: 2, priceCents: 450 }],
coupon: null as string | null,
}),
getters: {
count: (state) => state.lines.reduce((n, l) => n + l.quantity, 0),
isEmpty: (state) => state.lines.length === 0,
totalCents(): number {
return this.lines.reduce((sum, l) => sum + l.priceCents * l.quantity, 0)
},
formattedTotal(): string {
return `$${(this.totalCents / 100).toFixed(2)}`
},
},
actions: {
clear() {
this.lines = []
this.coupon = null
},
},
})
const cart = useCartStore()
// Output: 2
console.log(cart.count)
// Output: $9.00
console.log(cart.formattedTotal)One write at a time, or one $patch
Direct assignment and $patch do the same work; $patch groups it. Reach for the object form when several fields change together and you want one step in the devtools timeline, and the function form when you need to mutate an array or decide what to change while you are inside it.
const cart = useCartStore()
// Longhand: two assignments, two entries in devtools.
cart.coupon = 'SPRING'
cart.lines = []
// The same two changes in one entry. $patch merges the keys you pass.
cart.$patch({ coupon: 'SPRING', lines: [] })
// Function form, for arrays and anything conditional.
cart.$patch((state) => {
state.lines.push({ id: 'b2', quantity: 1, priceCents: 1200 })
state.coupon = null
})
// Output: 1
console.log(cart.lines.length)A getter that takes an argument
Getters are computed values, so they cannot accept parameters. Returning a function from the getter gets you the same calling shape. Note the cost: the returned function runs on every call, so nothing is cached. Use it for lookups, not for anything expensive.
// src/stores/cart.ts (getters section)
getters: {
lineById: (state) => {
return (id: string) => state.lines.find((line) => line.id === id)
},
},
// Used from a component or another store:
const cart = useCartStore()
// Output: 2
console.log(cart.lineById('a1')?.quantity)
Discussion