When multiple components need the same data, passing props gets messy. Pinia is Vue's official state management library — simpler than Vuex, works great with the Composition API. This lesson shows you how to build stores and share state across your whole app.
Imagine a shopping cart. The cart count shows in the navbar. The checkout button shows in the sidebar. The cart items list shows in the main view. All three components need the same data. You could pass it down through props, but that gets unwieldy fast (called 'prop drilling').
A store is a single place where shared state lives. Any component can read from it or write to it directly. Pinia is the official Vue store — it's lightweight, TypeScript-friendly, and integrates with Vue DevTools.
npm install pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // register Pinia with your app
app.mount('#app')
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// defineStore takes an ID (must be unique) and a setup function
export const useCartStore = defineStore('cart', () => {
// state: reactive variables
const items = ref([])
// getters: computed properties
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.qty, 0)
)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
)
// actions: functions that modify state
function addItem(product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.qty++
} else {
items.value.push({ ...product, qty: 1 })
}
}
function removeItem(productId) {
items.value = items.value.filter(i => i.id !== productId)
}
function clearCart() {
items.value = []
}
// Return everything the component can access
return { items, totalItems, totalPrice, addItem, removeItem, clearCart }
})
{{ product.name }}
${{ product.price }}
useCartStore() in multiple components and they all share the exact same instance. Change it in one component and every other component that uses it updates automatically. That's the whole point.Pinia actions can be async — just add async to the function. This is where you'd put API calls.
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('users', () => {
const users = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchUsers() {
loading.value = true
error.value = null
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
users.value = await res.json()
} catch (e) {
error.value = 'Failed to load users'
} finally {
loading.value = false
}
}
return { users, loading, error, fetchUsers }
})
Loading...
{{ store.error }}
- {{ user.name }}
ProductList.vue component with 6 hardcoded products and an 'Add to Cart' button on each.CartSidebar.vue that shows all items in the cart with quantity and price.useYourStore() in any component. They all share the same reactive instance.<script setup>.