Skip to content

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 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 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 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.

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.

  1. Add a todos ref (array of { id: string; text: string; done: boolean }) to a store.
  2. Add a completedCount computed that counts done todos.
  3. Add addTodo(text: string), toggleTodo(id: string), and removeTodo(id: string) actions.
  4. 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.