---
title: "Pinia from the inside: state, getters and actions · vuejs.school"
description: "What a Pinia store is, why useStore returns one object everywhere, how getters cache, what $patch and $subscribe do, and what devtools shows for each part."
canonical_url: "https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions"
meta:
  "og:description": "What a Pinia store is, why useStore returns one object everywhere, how getters cache, what $patch and $subscribe do, and what devtools shows for each part."
  "og:image": "https://vuejs.school/_og/d/a_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucw%2Cdescription_~V2hhdCBhIFBpbmlhIHN0b3JlIGlzLCB3aHkgdXNlU3RvcmUgcmV0dXJucyBvbmUgb2JqZWN0IGV2ZXJ5d2hlcmUsIGhvdyBnZXR0ZXJzIGNhY2hlLCB3aGF0ICRwYXRjaCBhbmQgJHN1YnNjcmliZSBkbywgYW5kIHdoYXQgZGV2dG9vbHMgc2hvd3MgZm9yIGVhY2ggcGFydC4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS1mcm9tLXRoZS1pbnNpZGUtc3RhdGUtZ2V0dGVycy1hY3Rpb25zIg%2Cs_f_DiFW1VWn5Ct6Gi.png"
  "og:image:alt": "Pinia from the inside: state, getters and actions · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Pinia from the inside: state, getters and actions · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucw%2Cdescription_~V2hhdCBhIFBpbmlhIHN0b3JlIGlzLCB3aHkgdXNlU3RvcmUgcmV0dXJucyBvbmUgb2JqZWN0IGV2ZXJ5d2hlcmUsIGhvdyBnZXR0ZXJzIGNhY2hlLCB3aGF0ICRwYXRjaCBhbmQgJHN1YnNjcmliZSBkbywgYW5kIHdoYXQgZGV2dG9vbHMgc2hvd3MgZm9yIGVhY2ggcGFydC4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS1mcm9tLXRoZS1pbnNpZGUtc3RhdGUtZ2V0dGVycy1hY3Rpb25zIg%2Cs_f_DiFW1VWn5Ct6Gi.png"
  "twitter:image:alt": "Pinia from the inside: state, getters and actions · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## A store outlives components

A component's state is born in its setup and dies when the component unmounts. A store's state does neither.

`createPinia()` creates one `pinia` instance. It holds a reactive object, `pinia.state`, with every store's state by id, and an internal map of the stores themselves. `app.use(pinia)` installs it in the app. When a store is first used, Pinia runs its setup function once, puts the state into `pinia.state.value['cart']`, and adds the resulting store object to the map. From then on it stays: components mount and unmount around it, and only a page load or an explicit `cart.$dispose()` removes it.

That explains the first surprise. If the cart "resets" when you move to another page, either it was never in the store (it was a local `ref` in a component that unmounted) or the page itself reloaded, as it does after a plain `<a href>` link (not a `<RouterLink>`) or a refresh, and the whole app started again.

It also explains the second. Every component that uses the cart holds a reference to the same object, so when two seem to disagree about `cart.total`, one of them copied the value out: `const { total } = cart` reads the number once and never updates. Read `cart.total` directly, or destructure with `storeToRefs(cart)`.

## State, getters and actions in a setup store

A setup store is a function. Inside it you write ordinary Composition API code, and Pinia reads what you return:

*src/stores/cart.ts*

```
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export interface Line {
  id: number
  name: string
  price: number
  qty: number
}

export const useCartStore = defineStore('cart', () => {
  const lines = ref<Line[]>([])

  const total = computed(() => lines.value.reduce((sum, l) => sum + l.price * l.qty, 0))
  const count = computed(() => lines.value.reduce((sum, l) => sum + l.qty, 0))

  function add(line: Omit<Line, 'qty'>) {
    const existing = lines.value.find((l) => l.id === line.id)
    if (existing) existing.qty++
    else lines.value.push({ ...line, qty: 1 })
  }

  function clear() {
    lines.value = []
  }

  return { lines, total, count, add, clear }
})
```

`**ref()**`

**State**The data the store holds.

`**computed()**`

**Getter**A cached value derived from state.

`**function**`

**Action**Something you call to change state.

Pinia decides which part each returned thing is by looking at what it is. A `ref` or a `reactive` object becomes state. A `computed` becomes a getter. A function becomes an action. The names you return are the names on the store: `cart.lines`, `cart.total`, `cart.add()`.

Return everything that is state. A `ref` you keep private is still reactive and still works inside the store, but Pinia does not know about it, so it is missing from devtools, from server-side rendering and from plugins. If you need a value hidden from the outside, return it anyway and treat it as private by convention.

## How `useCartStore()` finds the same store every time

`defineStore('cart', setup)` does not create a store. It returns a function, the one you named `useCartStore`, and remembers the id and the setup function. The store is created the first time that function is called.

Each call to `useCartStore()` does this, in order:

1. It finds the active Pinia instance. Inside a component it uses `inject()` to get the one installed with `app.use(pinia)`. Outside a component it uses the instance Pinia marked active: the one most recently installed or passed to `setActivePinia()`.
2. It looks in that instance's map of stores for the id `cart`.
3. If the store is there, it returns it. If not, it runs your setup function once, wraps the result into a store object, registers it in the map, and returns that.

So "the same instance everywhere" is a lookup in a map keyed by id, and the setup function is a factory that runs once per Pinia instance. On the server, where every request gets its own Pinia instance, the same code runs once per request, which is what stops one visitor seeing another visitor's cart.

## Why calling a store at the top of a file throws

Step 1 needs an active Pinia, and that is where beginners get caught. This file calls the store at its top level:

*src/api/client.ts**Breaks***

```
import { useAuthStore } from '@/stores/auth'

const auth = useAuthStore()

export function authHeaders() {
  return { Authorization: `Bearer ${auth.token}` }
}
```

Top-level code runs when a file is first imported, and every file that `main.ts` imports, directly or through another file, runs before `main.ts` reaches `app.use(pinia)`. In development the console shows `[🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?` A production build skips that check and fails with a vaguer `TypeError` instead.

The fix is to move the call inside the function, so it runs when something calls `authHeaders()`, long after Pinia is installed:

*src/api/client.ts**Works***

```
import { useAuthStore } from '@/stores/auth'

export function authHeaders() {
  const auth = useAuthStore()
  return { Authorization: `Bearer ${auth.token}` }
}
```

Calling `useAuthStore()` on every call is cheap: after the first time it is a lookup in the map.

## How getters cache

A getter is a `computed`, and a `computed` caches. `cart.total` runs its function once, remembers the result, and records that it depends on `lines` and on every `price` and `qty` it read. Reading `cart.total` ten times in a template runs the reduce once. Changing a `qty` marks the computed stale, and the next read runs it again. Nothing runs if nobody reads.

Two consequences follow. First, a getter can use another getter, and the dependency tracking still works through both: `const isEmpty = computed(() => count.value === 0)` depends on `count`, which depends on `lines`, so adding a line updates both.

Second, a getter that takes an argument cannot cache, because a `computed` has no arguments. The pattern is a getter that returns a function, written inside the setup function and returned like any getter:

```
const lineById = computed(() => (id: number) => lines.value.find((l) => l.id === id))
```

The outer `computed` caches the function, not the result, so `cart.lineById(3)` searches the array every time. That is usually fine; if it is not, build a `Map` inside the computed and look up in that.

## Actions can be async and call each other

An action is a plain function, so it can be `async`, it can throw, and it can call other actions by name, because they are in the same scope. This one goes inside the cart's setup function and is returned with the rest:

```
async function checkout() {
  const snapshot = lines.value.map((l) => ({ ...l }))
  clear()
  try {
    const res = await fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ lines: snapshot }),
    })
    if (!res.ok) throw new Error(`Order failed with status ${res.status}`)
  } catch (error) {
    lines.value = snapshot
    throw error
  }
}
```

`fetch` rejects only when the request fails, for example on a network error. A response such as a 500 still resolves, so the `res.ok` check turns it into an error. The `catch` then restores the lines and rethrows, so the component that called `checkout()` can show a message.

`clear()` is called by name, not through `this` or the store, so the call skips Pinia's action wrapper. The wrapper sits on the store object and tells `$onAction` subscribers, devtools among them, when an action starts and finishes. So when a component calls `cart.checkout()`, those subscribers see `checkout` and no separate `clear`.

## The `$` methods: `$patch`, `$subscribe` and `$reset`

Every store carries a few methods that start with `$`. These three are worth knowing early.

`$patch` applies several changes as one mutation. You can pass an object to merge or a function to mutate:

```
cart.$patch({ lines: [] })

cart.$patch((state) => {
  state.lines.push({ id: 9, name: 'Tea', price: 3, qty: 1 })
  state.lines[0].qty = 2
})
```

Direct assignment, `cart.lines = []`, also works for state, and for a single change it is the clearest. `$patch` matters when several changes belong together: subscribers receive them as one mutation with one type, and the devtools timeline shows one entry instead of one per change.

`$subscribe` runs a callback after the store's state changes. It receives a `mutation` object and the new state. `mutation.type` is `'direct'` for assignments, `'patch object'` or `'patch function'` for the two forms of `$patch`, and `mutation.storeId` names the store. Like a `watch`, the callback runs after the current code finishes, so several direct assignments made together arrive as one call; pass `{ flush: 'sync' }` for one call per change.

The common use is persistence. This `main.ts` saves the cart whenever it changes:

*src/main.ts*

```
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { useCartStore } from './stores/cart'

const app = createApp(App)
app.use(createPinia())

const cart = useCartStore()
cart.$subscribe((mutation, state) => {
  localStorage.setItem('cart', JSON.stringify(state.lines))
})

app.mount('#app')
```

`useCartStore()` works here, outside any component, because `app.use()` has just made this Pinia the active one. A subscription made inside a component is removed when the component unmounts. To keep it, pass `{ detached: true }` as the second argument, or create it in `main.ts` as above, where there is no component to unmount.

Setup stores do not get `$reset()` for free. In development, calling it throws `🍍: Store "cart" is built using the setup syntax and does not implement $reset().` In a production build it does nothing at all. If you need a reset, write a `reset` action that assigns the initial values, and return it like any other action.

## What the Pinia tab in devtools shows

Open the Vue devtools, pick the Pinia tab, and select the `cart` store. The inspector shows two of the three parts. State lists `lines` and lets you edit a value in place; the app reacts as if the code had changed it. Getters lists `total` and `count` with their current values, recomputed as you edit state.

Actions appear in the timeline instead. It records each action called on the store, with its arguments, and every mutation with its type, which is how you find out which action emptied the cart.

Because the inspector is built from what the store returned, a private `ref` is invisible, and so is a getter you did not return.

**Check it stuck**3 questions from the Setup stores quiz.

[**Practice now**](https://vuejs.school/learn/pinia/setup-stores?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**Upgrading a Pinia 3 project to Pinia 4, with the Nuxt and testing packages**](https://vuejs.school/community/blog/upgrading-a-pinia-3-project-to-pinia-4) [Release notes · 7 min read**Pinia 4: what changed and what to do**](https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-05-13","description":"What a Pinia store is, why useStore returns one object everywhere, how getters cache, what $patch and $subscribe do, and what devtools shows for each part.","name":"Pinia from the inside: state, getters and actions","url":"https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions","breadcrumb":{"@id":"https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-05-13","description":"What a Pinia store is, why useStore returns one object everywhere, how getters cache, what $patch and $subscribe do, and what devtools shows for each part.","headline":"Pinia from the inside: state, getters and actions","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucw%2Cdescription_~V2hhdCBhIFBpbmlhIHN0b3JlIGlzLCB3aHkgdXNlU3RvcmUgcmV0dXJucyBvbmUgb2JqZWN0IGV2ZXJ5d2hlcmUsIGhvdyBnZXR0ZXJzIGNhY2hlLCB3aGF0ICRwYXRjaCBhbmQgJHN1YnNjcmliZSBkbywgYW5kIHdoYXQgZGV2dG9vbHMgc2hvd3MgZm9yIGVhY2ggcGFydC4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS1mcm9tLXRoZS1pbnNpZGUtc3RhdGUtZ2V0dGVycy1hY3Rpb25zIg%2Cs_f_DiFW1VWn5Ct6Gi.png","@type":["Article","BlogPosting"],"articleSection":["Deep dive"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/pinia-from-the-inside-state-getters-actions#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Pinia from the inside: state, getters and actions","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucw%2Cdescription_~V2hhdCBhIFBpbmlhIHN0b3JlIGlzLCB3aHkgdXNlU3RvcmUgcmV0dXJucyBvbmUgb2JqZWN0IGV2ZXJ5d2hlcmUsIGhvdyBnZXR0ZXJzIGNhY2hlLCB3aGF0ICRwYXRjaCBhbmQgJHN1YnNjcmliZSBkbywgYW5kIHdoYXQgZGV2dG9vbHMgc2hvd3MgZm9yIGVhY2ggcGFydC4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS1mcm9tLXRoZS1pbnNpZGUtc3RhdGUtZ2V0dGVycy1hY3Rpb25zIg%2Cs_f_DiFW1VWn5Ct6Gi.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgZnJvbSB0aGUgaW5zaWRlOiBzdGF0ZSwgZ2V0dGVycyBhbmQgYWN0aW9ucw%2Cdescription_~V2hhdCBhIFBpbmlhIHN0b3JlIGlzLCB3aHkgdXNlU3RvcmUgcmV0dXJucyBvbmUgb2JqZWN0IGV2ZXJ5d2hlcmUsIGhvdyBnZXR0ZXJzIGNhY2hlLCB3aGF0ICRwYXRjaCBhbmQgJHN1YnNjcmliZSBkbywgYW5kIHdoYXQgZGV2dG9vbHMgc2hvd3MgZm9yIGVhY2ggcGFydC4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS1mcm9tLXRoZS1pbnNpZGUtc3RhdGUtZ2V0dGVycy1hY3Rpb25zIg%2Cs_f_DiFW1VWn5Ct6Gi.png"}]}
```