Introduction

If you've been building Vue.js applications for any length of time, you've probably used Vuex for state management. It was the go-to solution for years. But with Vue 3, a new challenger arrived — Pinia — and it's now the officially recommended state management library.

In this article, I'll compare both from a practical perspective, using real patterns I've used in production projects including a large-scale job portal built with Laravel + Vue 3.

ℹ️ Quick Context

Pinia is now the official state management library for Vue 3 as of 2023. Vuex 5 development has been paused and the team recommends migrating to Pinia for new projects.

Setup & Boilerplate

One of the first things you notice switching from Vuex to Pinia is how much less code you write. Let's see a simple user store in both:

Vuex 4 — User Store

store/user.js · Vuex 4
import { createStore } from 'vuex'

export default createStore({
  state: () => ({
    user: null,
    isLoading: false,
  }),
  getters: {
    isLoggedIn: (state) => !!state.user,
    userName:  (state) => state.user?.name,
  },
  mutations: {
    SET_USER(state, user) { state.user = user },
    SET_LOADING(state, val) { state.isLoading = val },
  },
  actions: {
    async fetchUser({ commit }) {
      commit('SET_LOADING', true)
      const res = await api.get('/user')
      commit('SET_USER', res.data)
      commit('SET_LOADING', false)
    }
  }
})

Pinia — Same Store (50% less code)

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

export const useUserStore = defineStore('user', () => {
  // State — just refs!
  const user      = ref(null)
  const isLoading = ref(false)

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

  // Actions — plain async functions
  async function fetchUser() {
    isLoading.value = true
    user.value = (await api.get('/user')).data
    isLoading.value = false
  }

  return { user, isLoading, isLoggedIn, userName, fetchUser }
})
✅ Key Difference

Pinia eliminates mutations entirely. You mutate state directly in actions. No more commit('SET_USER') boilerplate — just user.value = data.

TypeScript Support

If you're using TypeScript (and you should be for any large SPA), Pinia wins by a massive margin. Vuex 4's TypeScript support requires significant workarounds — manual type augmentation, typed dispatch wrappers, and so on.

stores/cart.ts · Pinia + TypeScript
interface CartItem {
  id:       number
  name:     string
  price:    number
  quantity: number
}

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
  )

  function addItem(item: CartItem) {
    const existing = items.value.find(i => i.id === item.id)
    if (existing) existing.quantity++
    else items.value.push(item)
  }

  return { items, total, addItem }
})

Full type inference, zero boilerplate. Your IDE autocompletes everything from useCartStore() directly.

DevTools & Debugging

Both have Vue DevTools integration, but the experience differs. Pinia shows each store independently with its state, getters, and a full action history. You can even time-travel debug in Pinia by inspecting action snapshots.

⚠️ Vuex DevTools Limitation

In Vuex, all state lives in one tree which can get overwhelming in large apps. Pinia's modular store approach keeps DevTools clean and focused.

Side-by-Side Comparison

Here's a full breakdown of both libraries across the features that matter most in production:

Feature Pinia Vuex 4
Vue 3 Support✓ Native~ Compatible
TypeScript✓ First-class✗ Workarounds needed
Boilerplate✓ Minimal✗ Verbose
Mutations required✓ No✗ Yes
Composition API✓ Native~ Limited
Modular by default✓ Yes~ Namespaced
SSR Support✓ Full✓ Full
Bundle size✓ ~1kb~ ~10kb
Official recommendation✓ Recommended✗ Maintenance only
Learning curve✓ Low~ Medium

Real-World Usage Pattern

Here's how I structure Pinia stores in a real Laravel + Vue 3 project. The pattern below handles API calls, error states, and loading indicators cleanly:

stores/jobs.js · Production Pattern
export const useJobStore = defineStore('jobs', () => {
  const jobs    = ref([])
  const loading = ref(false)
  const error   = ref(null)
  const meta    = ref({})  // pagination

  const totalJobs = computed(() => meta.value.total ?? 0)

  async function fetchJobs(params = {}) {
    loading.value = true
    error.value   = null
    try {
      const { data } = await api.get('/jobs', { params })
      jobs.value    = data.data
      meta.value    = data.meta
    } catch (err) {
      error.value = err.response?.data?.message ?? 'Failed to load jobs'
    } finally {
      loading.value = false
    }
  }

  return { jobs, loading, error, meta, totalJobs, fetchJobs }
})

Using the store in a component

JobList.vue
<script setup>
import { onMounted } from 'vue'
import { useJobStore } from '@/stores/jobs'

const jobStore = useJobStore()

onMounted(() => jobStore.fetchJobs())
</script>

<template>
  <div v-if="jobStore.loading">Loading...</div>
  <div v-else-if="jobStore.error">{{ jobStore.error }}</div>
  <ul v-else>
    <li v-for="job in jobStore.jobs" :key="job.id">
      {{ job.title }} <!-- clean, no mapGetters! -->
    </li>
  </ul>
</template>

When To Still Use Vuex

Pinia is the better choice for almost all new Vue 3 projects. However, there are still situations where Vuex makes sense:

  • You're maintaining a legacy Vue 2 codebase — Vuex 3 is built for Vue 2
  • Your team is already deeply familiar with Vuex patterns and migration isn't worth the effort
  • You have a very large existing codebase where a Vuex → Pinia migration would be high risk
🏁 Final Verdict

Use Pinia for all new Vue 3 projects. It's the official recommendation, has a fraction of the boilerplate, and TypeScript support is first-class. The learning curve is minimal if you already know the Composition API.

If you're on an existing Vuex project, migrate incrementally — Pinia can coexist with Vuex during transition. Start new features in Pinia stores and gradually replace Vuex modules.

👨‍💻
Sonu Sahani
Full Stack Developer · CodeCraft Systems

6+ years building production Laravel APIs, Vue.js SPAs & cloud SaaS products. Currently working with Lifelancer (UK). Writing about real-world patterns I use every day.