As a Vue application grows, more and more components need the same data: the logged-in user, the shopping cart, app settings. Passing that data through props and events across many layers quickly becomes tangled. Pinia is the official state management library for Vue 3. It gives you centralized stores that any component can read and update, with full reactivity and excellent devtools support. In this lesson you'll create a store, use it in components, and handle async data.
npm install piniaRegister it once in your entry file:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
createApp(App).use(createPinia()).mount('#app')A store is defined with defineStore. The option style has three parts: state (the data), getters (computed values), and actions (methods that change state):
// src/stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
getters: {
itemCount: (state) => state.items.length,
total: (state) => state.items.reduce((sum, i) => sum + i.price * i.qty, 0)
},
actions: {
addItem(product) {
const existing = this.items.find(i => i.id === product.id)
if (existing) existing.qty++
else this.items.push({ ...product, qty: 1 })
},
removeItem(id) {
this.items = this.items.filter(i => i.id !== id)
}
}
})Note that state is a function returning an object, exactly like data in the Options API. The first argument to defineStore is a unique id used by the devtools.
Call the store function inside setup (or <script setup>), then use it directly:
<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cart = useCartStore()
const { items, total } = storeToRefs(cart)
</script>
<template>
<p>{{ items.length }} items, total {{ total }}</p>
<button @click='cart.removeItem(1)'>Remove</button>
</template>One common pitfall: destructuring const { total } = cart breaks reactivity because you copy a plain value. Wrap the store with storeToRefs to keep state and getters reactive. Actions can be destructured directly since they are just functions.
If you prefer the Composition API style, define the store with a function. ref becomes state, computed becomes getters, and plain functions become actions:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, double, increment }
})Both styles are fully supported; pick one and stay consistent across your project.
Actions can be async, which makes them the natural home for API calls:
actions: {
async fetchProducts() {
this.loading = true
try {
const res = await fetch('/api/products')
this.products = await res.json()
} finally {
this.loading = false
}
}
}Components simply call store.fetchProducts() and render store.loading while the request is in flight.
Not every app needs a store. If only a parent and child share data, props and events are simpler. Reach for Pinia when several unrelated components need the same data, when state must survive route changes, or when you want time-travel debugging in the Vue devtools.
defineStore and call useXStore() in any component.storeToRefs when destructuring state or getters to keep reactivity.In the final lesson we take everything live: Building and Deploying a Vue App.