State Management with Pinia

advanced
12 min

State Management with Pinia

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.

Installing Pinia

bash
npm install pinia

Register it once in your entry file:

js
import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' createApp(App).use(createPinia()).mount('#app')

Defining a Store

A store is defined with defineStore. The option style has three parts: state (the data), getters (computed values), and actions (methods that change state):

js
// 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.

Using a Store in Components

Call the store function inside setup (or <script setup>), then use it directly:

html
<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.

Setup Stores

If you prefer the Composition API style, define the store with a function. ref becomes state, computed becomes getters, and plain functions become actions:

js
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.

Async Actions

Actions can be async, which makes them the natural home for API calls:

js
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.

When Do You Need Pinia?

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.

Key Takeaways

  • Pinia is the official Vue 3 store library: state, getters, and actions in one place.
  • Define stores with defineStore and call useXStore() in any component.
  • Use storeToRefs when destructuring state or getters to keep reactivity.
  • Async actions are the standard place for API calls and loading flags.
  • Prefer local component state until multiple distant components truly share data.

In the final lesson we take everything live: Building and Deploying a Vue App.

State Management with Pinia - Vue.js | CodeYourCraft | CodeYourCraft