Vue
04 / 08

State Management with Pinia

Vue State Management with Pinia

Pinia is the official state management library for Vue. It provides a simple, type-safe API and works seamlessly with the Composition API and Vue DevTools.

Setting Up Pinia

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

Defining a Store

Stores in Pinia are defined using the defineStore() function. There are two syntaxes: Option Store (similar to Vue Options API) and Setup Store (similar to Composition API).

// stores/counter.js - Option Store syntax
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter',
  }),
  
  getters: {
    doubleCount: (state) => state.count * 2,
    
    // Accessing other getters
    isEven() {
      return this.count % 2 === 0
    },
    
    // Getter with parameters
    countPlusN: (state) => (n) => state.count + n,
  },
  
  actions: {
    increment() {
      this.count++
    },
    
    async incrementAsync() {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      this.count++
    },
    
    reset() {
      this.count = 0
    },
  },
})

// stores/todos.js - Setup Store syntax (recommended)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTodoStore = defineStore('todos', () => {
  // State
  const todos = ref([])
  const filter = ref('all')
  const loading = ref(false)
  
  // Getters
  const completedTodos = computed(() =>
    todos.value.filter((todo) => todo.completed)
  )
  
  const activeTodos = computed(() =>
    todos.value.filter((todo) => !todo.completed)
  )
  
  const filteredTodos = computed(() => {
    switch (filter.value) {
      case 'completed':
        return completedTodos.value
      case 'active':
        return activeTodos.value
      default:
        return todos.value
    }
  })
  
  // Actions
  async function fetchTodos() {
    loading.value = true
    try {
      const response = await fetch('/api/todos')
      todos.value = await response.json()
    } finally {
      loading.value = false
    }
  }
  
  function addTodo(text) {
    todos.value.push({
      id: Date.now(),
      text,
      completed: false,
    })
  }
  
  function toggleTodo(id) {
    const todo = todos.value.find((t) => t.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
  
  function deleteTodo(id) {
    const index = todos.value.findIndex((t) => t.id === id)
    if (index > -1) {
      todos.value.splice(index, 1)
    }
  }
  
  function setFilter(newFilter) {
    filter.value = newFilter
  }
  
  return {
    // State
    todos,
    filter,
    loading,
    // Getters
    completedTodos,
    activeTodos,
    filteredTodos,
    // Actions
    fetchTodos,
    addTodo,
    toggleTodo,
    deleteTodo,
    setFilter,
  }
})

Using Stores in Components

<script setup>
import { useTodoStore } from '@/stores/todos'
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const todoStore = useTodoStore()
const counterStore = useCounterStore()

// Direct access to state and getters
console.log(todoStore.todos)
console.log(todoStore.filteredTodos)

// Call actions
todoStore.addTodo('Learn Pinia')
todoStore.toggleTodo(1)

// Destructuring (loses reactivity)
const { todos, filter } = todoStore // ❌ Not reactive

// Use storeToRefs to maintain reactivity
const { todos, filter, completedTodos } = storeToRefs(todoStore) // ✅ Reactive
const { addTodo, toggleTodo } = todoStore // Actions don't need storeToRefs

// Subscribe to state changes
todoStore.$subscribe((mutation, state) => {
  console.log('Store mutated:', mutation.type)
  console.log('New state:', state)
  
  // Persist to localStorage
  localStorage.setItem('todos', JSON.stringify(state.todos))
})

// Watch specific state
watch(
  () => todoStore.todos.length,
  (newLength) => {
    console.log(`Todos count: ${newLength}`)
  }
)
</script>

<template>
  <div>
    <h1>Todos ({{ todos.length }})</h1>
    
    <div>
      <button @click="() => setFilter('all')">All</button>
      <button @click="() => setFilter('active')">Active</button>
      <button @click="() => setFilter('completed')">Completed</button>
    </div>
    
    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="() => toggleTodo(todo.id)"
        />
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

Store Composition

Stores can use other stores, enabling modular and composable state management.

// stores/auth.js
export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(null)
  
  const isAuthenticated = computed(() => !!user.value)
  
  async function login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    })
    const data = await response.json()
    user.value = data.user
    token.value = data.token
  }
  
  function logout() {
    user.value = null
    token.value = null
  }
  
  return { user, token, isAuthenticated, login, logout }
})

// stores/cart.js - uses auth store
export const useCartStore = defineStore('cart', () => {
  const authStore = useAuthStore() // ✅ Use another store
  
  const items = ref([])
  
  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
  
  async function addItem(product) {
    if (!authStore.isAuthenticated) {
      throw new Error('Please login to add items')
    }
    
    items.value.push({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity: 1,
    })
    
    // Sync with backend
    await fetch('/api/cart', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${authStore.token}`,
      },
      body: JSON.stringify({ productId: product.id }),
    })
  }
  
  return { items, total, addItem }
})

Pinia Plugins

// Persistence plugin
import { createPinia } from 'pinia'

function persistencePlugin({ store }) {
  // Load persisted state
  const stored = localStorage.getItem(store.$id)
  if (stored) {
    store.$patch(JSON.parse(stored))
  }
  
  // Persist on change
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

const pinia = createPinia()
pinia.use(persistencePlugin)

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free