Pinia stores shared state and domain actions outside the component tree while integrating with Vue reactivity and developer tools. A store has a unique ID and exposes state, getters, and actions. Components should still keep temporary presentation state locally; a store is for data and behavior that multiple parts of the application coordinate.
After this lesson, you can define a store, consume its state without breaking reactivity, keep asynchronous writes in actions, and choose between local component state, a composable, and Pinia.
Pinia is the official state management library for Vue 3. It replaces Vuex with a simpler, more intuitive API. Pinia stores are like components without a template - they hold reactive state that any component can access.
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Setup store (Composition API style - recommended)
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
const history = ref([])
// Getters (computed)
const doubleCount = computed(() => count.value * 2)
const isPositive = computed(() => count.value > 0)
// Actions
function increment() {
count.value++
history.value.push(`+1 -> ${count.value}`)
}
function decrement() {
count.value--
history.value.push(`-1 -> ${count.value}`)
}
function reset() {
count.value = 0
history.value = []
}
function incrementBy(amount) {
count.value += amount
}
return { count, history, doubleCount, isPositive, increment, decrement, reset, incrementBy }
})
// Options store (Options API style)
export const useCounterOptionsStore = defineStore('counterOptions', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() { this.count++ },
async fetchCount() {
const res = await fetch('/api/count')
this.count = await res.json()
}
}
})
// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref(localStorage.getItem('token'))
const loading = ref(false)
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => user.value?.name || 'Guest')
async function login(email, password) {
loading.value = true
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const data = await res.json()
user.value = data.user
token.value = data.token
localStorage.setItem('token', data.token)
} finally {
loading.value = false
}
}
function logout() {
user.value = null
token.value = null
localStorage.removeItem('token')
}
return { user, token, loading, isLoggedIn, userName, login, logout }
})
<template>
<div>
<!-- Counter store -->
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment()">+1</button>
<button @click="counter.decrement()">-1</button>
<button @click="counter.reset()">Reset</button>
<!-- Auth store -->
<p v-if="auth.isLoggedIn">Hello, {{ auth.userName }}!</p>
<button v-if="!auth.isLoggedIn" @click="auth.login('alice@example.com', 'password')">
Login
</button>
<button v-else @click="auth.logout()">Logout</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
import { useAuthStore } from '@/stores/auth'
// Use stores - reactive, auto-updates template
const counter = useCounterStore()
const auth = useAuthStore()
// Destructure with storeToRefs (preserves reactivity)
import { storeToRefs } from 'pinia'
const { count, doubleCount } = storeToRefs(counter)
// Actions can be destructured directly (not reactive)
const { increment, reset } = counter
</script>
Model a store around a domain capability such as cart, session, or catalog rather than a page name. State holds the source values, getters derive values such as a total, and actions perform transitions such as addItem or checkout. Keep mutation rules inside actions when several components must follow the same policy.
A setup store uses refs for state, computed values for getters, and functions for actions. Return every state property that Pinia must track. An option store declares state as a function, which gives each application instance its own initial state and supports predictable reset behavior.
Do not destructure reactive state directly from the store object into ordinary variables. Use storeToRefs for state and getters; actions can be destructured normally because they are bound methods. This distinction prevents a component from displaying the initial value forever while the store changes elsewhere.
Actions may be asynchronous and can call other actions. Represent loading and failure deliberately when several components depend on the request. Prevent duplicate submissions at the action boundary and avoid leaving partially updated state when a request fails.
Decide when store data becomes stale and when it should reset. User-specific state should be cleared on logout. Route changes do not automatically destroy a store, which is useful for shared state but surprising when a developer expects page-local cleanup. Persist only the fields that genuinely need persistence and never put secrets in browser storage.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.