⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

Vue 3 Pinia State Management: The Complete Guide

February 15, 2022
vuepiniastate-managementjavascript
Vue 3 Pinia State Management: The Complete Guide

Vue 3 Pinia State Management: The Complete Guide

Pinia has become the official state management library for Vue.js, offering a more intuitive and type-safe alternative to Vuex. Let's explore how to use it effectively in your Vue 3 applications.

Why Pinia Over Vuex?

Pinia offers several advantages:

  • No mutations - Direct state modification is cleaner
  • Better TypeScript support - Full type inference out of the box
  • No modules - Each store is independent
  • Smaller bundle size - Only ~1KB gzipped
  • DevTools support - Full Vue DevTools integration

Installation and Setup

# npm
npm install pinia

# yarn
yarn add pinia

# pnpm
pnpm add pinia

Creating the Pinia Instance

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

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

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

Defining Stores

Pinia stores can be defined using Setup Stores or Option Stores. Let's explore both approaches.

Option Stores

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // State
  state: () => ({
    count: 0,
    name: 'My Counter',
  }),

  // Getters
  getters: {
    doubleCount: (state) => state.count * 2,
    
    // Using other getters
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    },
  },

  // Actions
  actions: {
    increment() {
      this.count++
    },
    
    async fetchCount() {
      const response = await fetch('/api/count')
      const data = await response.json()
      this.count = data.count
    },
  },
})

Setup Stores (Recommended)

// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  // Getters
  const isLoggedIn = computed(() => !!user.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email, password) {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      })
      user.value = await response.json()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
  }

  return { user, loading, error, isLoggedIn, userName, login, logout }
})

Using Stores in Components

Basic Usage

<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">Increment</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

Destructuring with storeToRefs

Always use storeToRefs when destructuring state or getters to maintain reactivity:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
  </div>
</template>

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

const store = useCounterStore()

// Reactive refs for state and getters
const { count, doubleCount } = storeToRefs(store)

// Actions can be destructured directly
const { increment } = store
</script>

Direct State Modification

<script setup>
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()

// Direct modification (no mutations needed!)
store.count = 100

// Multiple changes at once
store.$patch({
  count: 50,
  name: 'Updated Counter',
})

// $patch with a function
store.$patch((state) => {
  state.count++
  state.name = state.name.toUpperCase()
})
</script>

Composing Stores

Pinia stores can use other stores, enabling powerful composition patterns.

// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  const userStore = useUserStore()
  
  const items = ref([])
  
  const total = computed(() => 
    items.value.reduce((sum, item) => sum + item.price, 0)
  )
  
  function addItem(product) {
    if (!userStore.isLoggedIn) {
      throw new Error('Please login to add items to cart')
    }
    items.value.push(product)
  }
  
  return { items, total, addItem }
})

Using with Composition API

Pinia integrates seamlessly with Vue's Composition API:

<template>
  <div v-if="loading">Loading...</div>
  <div v-else>
    <p>Welcome, {{ userName }}!</p>
    <button @click="handleLogout">Logout</button>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
const { userName, loading } = storeToRefs(userStore)
const { logout } = userStore

function handleLogout() {
  logout()
  // Redirect to login page
}
</script>

Plugins

Pinia supports plugins to extend functionality.

Persistence Plugin

// plugins/persist.js
import { createPinia } from 'pinia'

const pinia = createPinia()

pinia.use(({ store }) => {
  // Load from localStorage
  const stored = localStorage.getItem(store.$id)
  if (stored) {
    store.$patch(JSON.parse(stored))
  }
  
  // Watch for changes
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
})

export default pinia

Using the Plugin

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

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

Resetting State

Each store comes with a $reset method:

const store = useCounterStore()

// Reset to initial state
store.$reset()

Subscribing to Changes

State Subscriptions

const store = useCounterStore()

// Subscribe to state changes
store.$subscribe((mutation, state) => {
  console.log('Type:', mutation.type)
  console.log('Store ID:', mutation.storeId)
  console.log('New State:', state)
})

Action Subscriptions

const store = useCounterStore()

// Subscribe to actions
store.$onAction(({ name, args, after, onError }) => {
  console.log(`Action ${name} called with:`, args)
  
  after((result) => {
    console.log(`Action ${name} finished with:`, result)
  })
  
  onError((error) => {
    console.error(`Action ${name} failed:`, error)
  })
})

Best Practices

1. One Store Per Domain

// Good: Domain-based stores
stores/
  ├── user.js
  ├── cart.js
  └── products.js

// Avoid: Single giant store
stores/
  └── index.js  // Too much in one file

2. Use Composables for Complex Logic

// composables/useAuth.js
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'

export function useAuth() {
  const userStore = useUserStore()
  const router = useRouter()

  async function login(credentials) {
    await userStore.login(credentials)
    router.push('/dashboard')
  }

  return { login }
}

3. Type Your Stores Properly

// stores/user.ts
import { defineStore } from 'pinia'

interface User {
  id: number
  name: string
  email: string
}

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null as User | null,
  }),
  
  getters: {
    isLoggedIn: (state): boolean => state.user !== null,
  },
})

Conclusion

Pinia provides a clean, intuitive approach to state management in Vue 3 applications. Its Composition API-style syntax, excellent TypeScript support, and plugin ecosystem make it the ideal choice for modern Vue applications.

Start migrating your Vuex stores to Pinia today and enjoy a more pleasant development experience!

Share:

💬 Comments