Pinia
A store holds state that many components share; Pinia is the store library the Vue team recommends, installed once on the app.
const useCartStore = defineStore('cart', optionsOrSetupFunction)A store is an object that holds state outside the component tree, so any component can read it and change it. Props and events are enough while data travels one or two levels. When a header badge, a sidebar and a checkout page all need the same cart, threading it through every component in between stops being worth it.
Installing it
Pinia is a separate package: npm install pinia. You create one instance with createPinia() and hand it to the application with app.use(pinia). That instance is where every store's state actually lives.
Defining a store
defineStore() takes two arguments: a unique id string, and either an options object or a setup function. It returns a function that you call inside a component, named useCartStore by convention. Calling it twice returns the same store, not a second copy.
If you have seen Vuex
Pinia is the successor to Vuex, which is no longer actively developed. State, getters and actions carry over with the same meaning. Mutations are gone: an action changes state directly, so there is no commit and no dispatch. Modules are gone too, because every store is already its own module, which removes namespaced strings and this.$store.
What trips people up
Calling useCartStore() at the top level of a module runs it before app.use(pinia), when no Pinia instance is active yet. Call it inside a component, a composable, or an action.
Example
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
<button type="button" class="badge">
Cart ({{ cart.count }})
</button>
</template>
<style scoped>
.badge {
font: inherit;
padding: 0.25rem 0.75rem;
}
</style>When to use it
- Keeping the signed-in user available to the header, a profile page and a route guard without passing props through every layer.
- Holding a shopping cart that a product card writes to and a checkout page reads.
- Sharing UI state, such as whether the sidebar is collapsed, between components that are not related.
More examples
Installing Pinia on the app
One Pinia instance per application, installed before mount. Reach for this once, in the entry file. Every store you later define reads and writes its state through this instance, which is also why a store function called on import has nothing to attach to.
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')The store the component above reads
One file per store, one store per domain. The id 'cart' is what devtools labels and what plugins key on, so it has to be unique across the app. Any component can now call useCartStore() and see the same lines.
// src/stores/cart.ts
import { defineStore } from 'pinia'
interface Line {
id: string
quantity: number
priceCents: number
}
export const useCartStore = defineStore('cart', {
state: () => ({
lines: [] as Line[],
}),
getters: {
count: (state) => state.lines.reduce((n, line) => n + line.quantity, 0),
},
actions: {
add(line: Line) {
const found = this.lines.find((l) => l.id === line.id)
if (found) {
found.quantity += line.quantity
return
}
this.lines.push(line)
},
},
})The older Vuex shape, for recognition
Useful when you are reading an older project or a blog post. The two halves do the same job. In Pinia an action writes state directly and is called like a method, so the commit and dispatch indirection disappears.
// Vuex 4, the older form. You will meet it in existing codebases.
const store = createStore({
state: () => ({ count: 0 }),
mutations: {
increment(state) {
state.count++
},
},
actions: {
bump({ commit }) {
commit('increment')
},
},
})
// store.commit('increment')
// store.dispatch('bump')
// The same store in Pinia: no mutations, no commit, no dispatch.
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
},
bump() {
this.increment()
},
},
})
// const counter = useCounterStore()
// counter.increment()
Discussion