---
title: "Setup stores or option stores? Pick one and stay consistent · vuejs.school"
description: "The same cart store written as an option store and as a setup store, how the parts map, the one rule setup stores add, and why new code should use them."
canonical_url: "https://vuejs.school/community/blog/setup-stores-or-option-stores"
meta:
  "og:description": "The same cart store written as an option store and as a setup store, how the parts map, the one rule setup stores add, and why new code should use them."
  "og:image": "https://vuejs.school/_og/d/a_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQ%2Cdescription_~VGhlIHNhbWUgY2FydCBzdG9yZSB3cml0dGVuIGFzIGFuIG9wdGlvbiBzdG9yZSBhbmQgYXMgYSBzZXR1cCBzdG9yZSwgaG93IHRoZSBwYXJ0cyBtYXAsIHRoZSBvbmUgcnVsZSBzZXR1cCBzdG9yZXMgYWRkLCBhbmQgd2h5IG5ldyBjb2RlIHNob3VsZCB1c2UgdGhlbS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZXR1cC1zdG9yZXMtb3Itb3B0aW9uLXN0b3JlcyI%2Cs_IjWzo_2WtGJuVXnm.png"
  "og:image:alt": "Setup stores or option stores? Pick one and stay consistent · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Setup stores or option stores? Pick one and stay consistent · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQ%2Cdescription_~VGhlIHNhbWUgY2FydCBzdG9yZSB3cml0dGVuIGFzIGFuIG9wdGlvbiBzdG9yZSBhbmQgYXMgYSBzZXR1cCBzdG9yZSwgaG93IHRoZSBwYXJ0cyBtYXAsIHRoZSBvbmUgcnVsZSBzZXR1cCBzdG9yZXMgYWRkLCBhbmQgd2h5IG5ldyBjb2RlIHNob3VsZCB1c2UgdGhlbS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZXR1cC1zdG9yZXMtb3Itb3B0aW9uLXN0b3JlcyI%2Cs_IjWzo_2WtGJuVXnm.png"
  "twitter:image:alt": "Setup stores or option stores? Pick one and stay consistent · vuejs.school"
---

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

## The cart as an option store

The example is a small shopping cart, written once in each style: a list of items and a coupon code, two derived values (`count` and `total`), and actions to add and remove items.

*stores/cart.ts*

```
import { defineStore } from 'pinia'

export interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[],
    coupon: '',
  }),
  getters: {
    count: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
    total(state): number {
      const sum = state.items.reduce((s, item) => s + item.price * item.quantity, 0)
      return this.coupon === 'VUE10' ? sum * 0.9 : sum
    },
  },
  actions: {
    add(item: Omit<CartItem, 'quantity'>) {
      const existing = this.items.find(i => i.id === item.id)
      if (existing) existing.quantity++
      else this.items.push({ ...item, quantity: 1 })
    },
    remove(id: number) {
      this.items = this.items.filter(i => i.id !== id)
    },
  },
})
```

This reads like the Options API, and if you have worked with Vuex, every word here is familiar:

- `state` is a function that returns the initial object, the same way `data()` does in a component.
- `getters` are computed values. Each one receives the state as its argument.
- `actions` are methods, and `this` inside them is the store.

A getter written as a regular function can also read the rest of the store through `this`, as `total` does to read `coupon`. When it does, TypeScript needs the return type written out, as in `total(state): number`. Leave it off and the store's type loses its getters: `store.count` and `store.total` become type errors in the components that use them.

## The same cart as a setup store

*stores/cart.ts*

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

export interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const coupon = ref('')

  const count = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0))
  const total = computed(() => {
    const sum = items.value.reduce((s, item) => s + item.price * item.quantity, 0)
    return coupon.value === 'VUE10' ? sum * 0.9 : sum
  })

  function add(item: Omit<CartItem, 'quantity'>) {
    const existing = items.value.find(i => i.id === item.id)
    if (existing) existing.quantity++
    else items.value.push({ ...item, quantity: 1 })
  }

  function remove(id: number) {
    items.value = items.value.filter(i => i.id !== id)
  }

  return { items, coupon, count, total, add, remove }
})
```

This reads like `<script setup>`. The second argument is a function. Inside it you write ordinary Composition API code, with `.value` on every ref, and at the end you return the things the store should expose. There is no `this`, no annotation on `total`, and `count` and `total` are typed by inference, the same way they would be in a component.

## How the pieces map

The two files describe one store, and the correspondence is exact:

- A `ref()` you return becomes a state property. `items` and `coupon` appear in `store.$state` (the object that holds all of the store's state), in devtools and in anything a plugin persists.
- A `computed()` you return becomes a getter. `store.count` reads as a number, with no `.value`, because the store unwraps refs.
- A function you return becomes an action. It is bound to the store, so `const { add } = useCartStore()` works and so does passing `store.add` as a callback.

Code that uses the store is identical for both styles:

*components/CartBadge.vue*

```
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'

const cart = useCartStore()
const { count, total } = storeToRefs(cart)
</script>

<template>
  <button type="button" @click="cart.add({ id: 1, name: 'Mug', price: 12 })">
    {{ count }} items, {{ total.toFixed(2) }}
  </button>
</template>
```

`storeToRefs` pulls reactive refs out of the store so that destructuring does not lose reactivity, and it skips actions, which you call on the store itself. Whether `cart` came from an options object or a setup function makes no difference to this file.

## What setup stores can do that option stores cannot

The Pinia [documentation](https://pinia.vuejs.org/core-concepts/) says you can pick whichever style you are more comfortable with, calls option stores easier to work with, and calls setup stores more flexible and powerful. The flexibility comes down to three things.

First, you can call composables inside a setup store. A store that needs the current route calls `useRoute()`; a store that needs something provided at the app level calls `inject()`. The docs ask you not to return those from the store, because components can reach them directly, but you can use them to compute state.

An option store can call a composable only inside `state()`, and only one that returns writable state, such as VueUse's `useLocalStorage`. The docs' page on [composables in stores](https://pinia.vuejs.org/cookbook/composables.html) lists ones that do not fit, such as composables that return functions or read-only values.

Second, you can create watchers. A cart that saves itself is three lines inside the setup function in `stores/cart.ts` (add `watch` to the import from `vue`):

```
watch(items, (value) => {
  localStorage.setItem('cart', JSON.stringify(value))
}, { deep: true })
```

The watcher lives as long as the store, which is as long as the app. In an app that also renders on the server, guard browser APIs like `localStorage` with a check for `window`, or run the watcher from a client-only plugin.

Third, you can have private helpers that work on the store's refs directly. A function you do not return is invisible to components and to devtools, which is a clean way to keep a parsing routine or a validation step out of the public surface. This works for functions only. State cannot be private, as the next section shows.

## The one rule: return everything that is state

The setup syntax has one rule that the options syntax enforces for you. Every ref that holds state must be in the returned object. This version runs, and is wrong:

*stores/cart.ts**Breaks***

```
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const coupon = ref('')

  const total = computed(() => {
    const sum = items.value.reduce((s, item) => s + item.price * item.quantity, 0)
    return coupon.value === 'VUE10' ? sum * 0.9 : sum
  })

  function applyCoupon(code: string) {
    coupon.value = code
  }

  return { items, total, applyCoupon }
})
```

`coupon` is used, changed by an action and read by a getter, but never returned. Pinia does not know it exists, so it is missing from `store.$state`. That means:

- devtools cannot show or edit it;
- `$patch` cannot set it;
- a persistence plugin will not save it;
- with server-side rendering its value is lost, because only `$state` is serialised and sent to the browser.

The docs say the same about wrapping state in `readonly()` before returning it: it breaks SSR, devtools and plugins in the same way.

The fix is one word:

*stores/cart.ts**Works***

```
 return { items, coupon, total, applyCoupon }
```

If you want a piece of state to look private, name it with a leading underscore and leave it out of the components by convention. Do not leave it out of the return.

## Write your own `$reset` in a setup store

Every option store has a `$reset()` method. It calls your `state()` function again and replaces the current state with the fresh object, which is exactly what a logout button needs.

A setup store has no `state()` function to call again, so Pinia cannot write `$reset` for you. Calling it on a setup store throws in development, with the message `🍍: Store "cart" is built using the setup syntax and does not implement $reset().` In a production build the call does nothing at all, so a logout that relies on it would leave the old cart in place. Write the method yourself in `stores/cart.ts` and return it under the same name:

*stores/cart.ts*

```
 function $reset() {
    items.value = []
    coupon.value = ''
  }

  return { items, coupon, count, total, add, remove, $reset }
```

Keeping the name `$reset` means the code that resets stores on logout does not need to know which style each store uses. The price of writing it by hand: when you add a new ref to the store, you have to remember to add it to `$reset` too.

## Pick setup stores for new code

For a new store, use the setup syntax. It is the same Composition API you already write in components, it gives you composables and watchers when you need them, its getters need no type annotations, and the one rule it adds is easy to follow once you know it. The cost is `$reset`, a four-line function.

If your project already has option stores, do not rewrite them for the sake of it. They work, Pinia 3 supports them fully, and a rewrite without a reason is a way to introduce bugs. Choose one style for new stores, write it down in the project's contributing notes, and convert an old store only when you are changing it anyway. The mapping above makes that conversion mechanical: state to refs, getters to computeds, actions to functions, a return object at the end that lists every ref, and a `$reset` if anything calls it.

**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/setup-stores-or-option-stores#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-04-08","description":"The same cart store written as an option store and as a setup store, how the parts map, the one rule setup stores add, and why new code should use them.","name":"Setup stores or option stores? Pick one and stay consistent","url":"https://vuejs.school/community/blog/setup-stores-or-option-stores","breadcrumb":{"@id":"https://vuejs.school/community/blog/setup-stores-or-option-stores#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/setup-stores-or-option-stores"]}],"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/setup-stores-or-option-stores#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-04-08","description":"The same cart store written as an option store and as a setup store, how the parts map, the one rule setup stores add, and why new code should use them.","headline":"Setup stores or option stores? Pick one and stay consistent","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQ%2Cdescription_~VGhlIHNhbWUgY2FydCBzdG9yZSB3cml0dGVuIGFzIGFuIG9wdGlvbiBzdG9yZSBhbmQgYXMgYSBzZXR1cCBzdG9yZSwgaG93IHRoZSBwYXJ0cyBtYXAsIHRoZSBvbmUgcnVsZSBzZXR1cCBzdG9yZXMgYWRkLCBhbmQgd2h5IG5ldyBjb2RlIHNob3VsZCB1c2UgdGhlbS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZXR1cC1zdG9yZXMtb3Itb3B0aW9uLXN0b3JlcyI%2Cs_IjWzo_2WtGJuVXnm.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/setup-stores-or-option-stores#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/setup-stores-or-option-stores#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/setup-stores-or-option-stores#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Setup stores or option stores? Pick one and stay consistent","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_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQ%2Cdescription_~VGhlIHNhbWUgY2FydCBzdG9yZSB3cml0dGVuIGFzIGFuIG9wdGlvbiBzdG9yZSBhbmQgYXMgYSBzZXR1cCBzdG9yZSwgaG93IHRoZSBwYXJ0cyBtYXAsIHRoZSBvbmUgcnVsZSBzZXR1cCBzdG9yZXMgYWRkLCBhbmQgd2h5IG5ldyBjb2RlIHNob3VsZCB1c2UgdGhlbS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZXR1cC1zdG9yZXMtb3Itb3B0aW9uLXN0b3JlcyI%2Cs_IjWzo_2WtGJuVXnm.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~U2V0dXAgc3RvcmVzIG9yIG9wdGlvbiBzdG9yZXM~IFBpY2sgb25lIGFuZCBzdGF5IGNvbnNpc3RlbnQ%2Cdescription_~VGhlIHNhbWUgY2FydCBzdG9yZSB3cml0dGVuIGFzIGFuIG9wdGlvbiBzdG9yZSBhbmQgYXMgYSBzZXR1cCBzdG9yZSwgaG93IHRoZSBwYXJ0cyBtYXAsIHRoZSBvbmUgcnVsZSBzZXR1cCBzdG9yZXMgYWRkLCBhbmQgd2h5IG5ldyBjb2RlIHNob3VsZCB1c2UgdGhlbS4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZXR1cC1zdG9yZXMtb3Itb3B0aW9uLXN0b3JlcyI%2Cs_IjWzo_2WtGJuVXnm.png"}]}
```