Skip to content
All essays
WebMarch 27, 202513 min

Pinia State Management in Vue 3

Master Pinia, the official state management library for Vue 3. Learn stores, actions, getters, and best practices.

Ü
Ümit Uz
Mobile & Full Stack Developer

Pinia is the official state management library for Vue 3, replacing Vuex with a simpler and more TypeScript-friendly API.

Why Pinia?

  • Simple API - No mutations, just actions
  • TypeScript support - Excellent type inference
  • DevTools integration - Time-travel debugging
  • Lightweight - Small bundle size
  • Modular - Stores are independent by default

Installation

bash
npm install pinia

Setup

javascript
// 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 a Store

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

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

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

  // Actions
  function setUser(userData, userToken) {
    user.value = userData
    token.value = userToken
  }

  function logout() {
    user.value = null
    token.value = null
  }

  async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`)
    const data = await response.json()
    setUser(data.user, data.token)
  }

  return {
    user,
    token,
    isAuthenticated,
    userName,
    setUser,
    logout,
    fetchUser
  }
})

Options Store

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

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    total: 0
  }),

  getters: {
    itemCount: (state) => state.items.length,
    totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price, 0)
  },

  actions: {
    addItem(item) {
      this.items.push(item)
      this.calculateTotal()
    },

    removeItem(index) {
      this.items.splice(index, 1)
      this.calculateTotal()
    },

    calculateTotal() {
      this.total = this.items.reduce((sum, item) => sum + item.price, 0)
    }
  }
})

Using Stores in Components

vue
<template>
  <div>
    <div v-if="user">
      <p>Welcome, {{ userName }}!</p>
      <button @click="logout">Logout</button>
    </div>
    <div v-else>
      <button @click="login">Login</button>
    </div>
  </div>
</template>

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

const userStore = useUserStore()

const { user, userName } = storeToRefs(userStore)

async function login() {
  await userStore.fetchUser(1)
}

function logout() {
  userStore.logout()
}
</script>

Store Composition

You can use stores inside other stores:

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

export const useOrderStore = defineStore('order', () => {
  const userStore = useUserStore()
  const cartStore = useCartStore()

  const orders = ref([])

  async function createOrder() {
    if (!userStore.isAuthenticated) {
      throw new Error('User must be logged in')
    }

    const order = {
      user: userStore.user,
      items: cartStore.items,
      total: cartStore.totalPrice
    }

    const response = await fetch('/api/orders', {
      method: 'POST',
      body: JSON.stringify(order)
    })

    const data = await response.json()
    orders.value.push(data.order)
    cartStore.clear()

    return data.order
  }

  return {
    orders,
    createOrder
  }
})

Actions with Async/Await

javascript
// stores/product.js
import { defineStore } from 'pinia'

export const useProductStore = defineStore('product', () => {
  const products = ref([])
  const loading = ref(false)
  const error = ref(null)

  async function fetchProducts() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch('/api/products')
      const data = await response.json()
      products.value = data.products
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  return {
    products,
    loading,
    error,
    fetchProducts
  }
})

Store Plugins

javascript
// plugins/pinia-persist.js
import { watch } from 'vue'

export function createPersistedState(options = {}) {
  return ({ store }) => {
    const key = options.key ?? `pinia-${store.$id}`
    const storage = options.storage ?? localStorage

    // Restore state
    const savedState = storage.getItem(key)
    if (savedState) {
      store.$patch(JSON.parse(savedState))
    }

    // Save state
    store.$subscribe((mutation, state) => {
      storage.setItem(key, JSON.stringify(state))
    })
  }
}

// Usage in main.js
import { createPinia } from 'pinia'
import { createPersistedState } from './plugins/pinia-persist'

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

Testing Stores

javascript
// tests/stores/user.spec.js
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('sets user correctly', () => {
    const store = useUserStore()
    const user = { name: 'John', email: 'john@example.com' }

    store.setUser(user, 'token123')

    expect(store.user).toEqual(user)
    expect(store.token).toBe('token123')
    expect(store.isAuthenticated).toBe(true)
  })

  it('logs out correctly', () => {
    const store = useUserStore()
    store.setUser({ name: 'John' }, 'token')

    store.logout()

    expect(store.user).toBeNull()
    expect(store.token).toBeNull()
    expect(store.isAuthenticated).toBe(false)
  })
})

Best Practices

  1. 1Use setup stores for better TypeScript inference
  2. 2Keep stores focused - one store per domain
  3. 3Use composables for reusable logic across stores
  4. 4Avoid nested state - keep state flat
  5. 5Use storeToRefs when destructuring in components

Pinia provides a simple and powerful state management solution for Vue 3 applications with excellent TypeScript support and developer experience.

Related essays

Next essay
Vue Router 4: Complete Guide