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.
data() in components)// 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>
Understanding Pinia is not just about syntax. In production applications, this topic directly affects maintainability, debugging speed, and team collaboration. Focus on readability, small reusable patterns, and predictable state flow when implementing Pinia.
A practical approach is to first implement the simplest working version, then refactor into reusable pieces (components/composables/stores) only when duplication appears. This helps keep your Vue codebase clean while avoiding over-engineering.
Explore 500+ free tutorials across 20+ languages and frameworks.