Setup Stores
A setup store is defined by a function instead of an options object, using ref, computed and plain functions like a composable.
defineStore('filters', () => { /* refs, computed, functions */ return { /* exposed */ } })defineStore() accepts a function in place of the options object. Inside it you write the same code you would write in <script setup>, then return what the store should expose. This is a setup store, and it is the form this course uses.
How it maps onto the option style
| Option store | Setup store |
|---|---|
state: () => ({ query: '' }) | const query = ref('') |
getters: { summary } | const summary = computed(...) |
actions: { reset } | function reset() {} |
Both calls produce the same store object, so a component cannot tell which form you used. On the store, refs are unwrapped: you write filters.query, not filters.query.value. Inside the setup function it is still a ref, so query.value applies there.
Why reach for it
Because it is just a function, you can call watch() inside it, use a composable, or keep a helper that nobody outside needs. A store written this way reads like a composable whose state happens to be shared by the whole application rather than created fresh per caller.
What trips people up
Pinia only knows about what you return. A ref left out is missing from devtools and from the server-rendered payload. There is also no generated $reset() for a setup store: return your own function named $reset if you want one.
Example
<script setup lang="ts">
import { useFiltersStore } from '@/stores/filters'
const filters = useFiltersStore()
</script>
<template>
<form @submit.prevent>
<label for="q">Search</label>
<input id="q" v-model="filters.query" type="search" />
<p>{{ filters.summary }}</p>
<button type="button" @click="filters.reset()">Reset</button>
</form>
</template>
<style scoped>
form {
display: grid;
gap: 0.5rem;
max-width: 24rem;
}
</style>When to use it
- Writing a store that needs a watcher, so the rule lives in the store instead of in every component that writes to it.
- Reusing an existing composable, such as one that wraps a WebSocket, inside shared state.
- Keeping a helper function private to the store file while still exposing the actions that use it.
More examples
The setup store
Note the two levels. Inside the setup function query is a ref, so reset() writes query.value. From outside, the store unwraps it, so filters.query = 'boots' is the whole assignment. Everything the component touches is in the returned object.
// src/stores/filters.ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const useFiltersStore = defineStore('filters', () => {
const query = ref('')
const tags = ref<string[]>([])
const summary = computed(() =>
query.value === '' ? 'Showing everything' : `Showing "${query.value}"`,
)
function reset() {
query.value = ''
tags.value = []
}
return { query, tags, summary, reset }
})
const filters = useFiltersStore()
filters.query = 'boots'
// Output: Showing "boots"
console.log(filters.summary)The same store in the option style
Line for line the same store, so you can recognise it in other people's code. The option style has no .value anywhere and reaches state through this. The trade is flexibility: there is no place here to call watch() or to use another composable, which is the main reason to prefer the setup form.
// src/stores/filters.ts, written the other way
import { defineStore } from 'pinia'
export const useFiltersStore = defineStore('filters', {
state: () => ({
query: '',
tags: [] as string[],
}),
getters: {
summary(): string {
return this.query === ''
? 'Showing everything'
: `Showing "${this.query}"`
},
},
actions: {
reset() {
this.query = ''
this.tags = []
},
},
})A watcher and a hand-written $reset
Two things an options object cannot do. The watcher lives in the store, so the page reset happens wherever the query is changed from, and no component has to remember it. $reset is generated only for option stores, so here you return a function with that name and callers use store.$reset() exactly as they would elsewhere.
// src/stores/results.ts
import { ref, watch } from 'vue'
import { defineStore } from 'pinia'
export const useResultsStore = defineStore('results', () => {
const query = ref('')
const page = ref(1)
// Any new search starts again at page one.
watch(query, () => {
page.value = 1
})
function $reset() {
query.value = ''
page.value = 1
}
return { query, page, $reset }
})
Discussion