State, Getters, and Actions
In Pinia’s setup syntax, the mapping to familiar Composition API concepts is direct: state is ref, getters are computed, and actions are functions. This makes stores feel natural if you already know Vue’s reactivity system.
State — ref
Section titled “State — ref”State is any reactive data the store owns. In FamilyTree:
const people = ref<Person[]>([])const focusPersonId = ref<string | null>(null)State is reactive — components that read it re-render when it changes. Only actions should mutate state; components call actions rather than mutating state directly.
Getters — computed
Section titled “Getters — computed”Getters derive values from state. They are cached — they only re-run when their dependencies change:
const focusPerson = computed<Person | undefined>(() => people.value.find(p => p.id === focusPersonId.value))
const parents = computed<Person[]>(() => focusPerson.value ? people.value.filter(p => (focusPerson.value!.parentIds ?? []).includes(p.id)) : [])
const children = computed<Person[]>(() => focusPerson.value ? people.value.filter(p => (p.parentIds ?? []).includes(focusPerson.value!.id)) : [])
const currentSpouse = computed<Person | undefined>(() => { const rel = (focusPerson.value?.spouses ?? []).find(s => !s.divorceDate) return rel ? people.value.find(p => p.id === rel.personId) : undefined})No data is stored redundantly. Parents, children, siblings, spouses — all derived on demand from people. If you add a parent by pushing to person.parentIds, the parents computed updates automatically.
Actions — functions
Section titled “Actions — functions”Actions are plain functions. They can read state, call other actions, and mutate state:
function setFocus(id: string): void { focusPersonId.value = id}
function addPerson(data: PersonFormData): string { const id = crypto.randomUUID() people.value.push({ ...data, id }) if (focusPersonId.value === null) focusPersonId.value = id return id}
function deletePerson(id: string): void { people.value = people.value.filter(p => p.id !== id) // also clean up references in other people if (focusPersonId.value === id) { focusPersonId.value = people.value[0]?.id ?? null }}
function addParent(personId: string, parentId: string): void { const person = people.value.find(p => p.id === personId) if (!person || person.parentIds.length >= 2) return person.parentIds.push(parentId)}Actions are synchronous in FamilyTree (no HTTP calls), but they can be async — async function loadPeople() { ... } works normally.
What to return
Section titled “What to return”Return everything that components need — state, getters, and actions:
return { // state people, focusPersonId, // getters focusPerson, parents, children, currentSpouse, siblings, // actions addPerson, updatePerson, deletePerson, setFocus, addParent, // ... more actions}Anything not returned is private to the store — you can have internal helpers that aren’t exposed.
Exercise
Section titled “Exercise”- Add a
todosref (array of{ id: string; text: string; done: boolean }) to a store. - Add a
completedCountcomputed that counts done todos. - Add
addTodo(text: string),toggleTodo(id: string), andremoveTodo(id: string)actions. - Return all of them and verify they work from a component.
ref()in a setup store = state — reactive and owned by the store.computed()= getters — derived, cached, auto-updating.- Functions = actions — read and mutate state; can be async.
- Return everything components need; unreturned values are private.
- Derive data in getters rather than storing it redundantly.