State Management in Vue.js 3: Moving From Vuex to Pinia

I am a Software developer and I'm passionate about learning new things.
One of the most significant architectural shifts in the Vue ecosystem is the move from Vuex to Pinia.
If you're migrating from Vue 2 to Vue 3, this change often raises a few practical questions:
Do you have to switch?
Is Vuex obsolete?
What actually changes in real code?
Let’s walk through the technical differences and look at how state management evolves in Vue 3.
Why Pinia Exists
Pinia was created to simplify state management while keeping the core ideas of centralized stores.
Vuex introduced a strict pattern:
State
Getters
Mutations
Actions
This structure worked well for predictability, but in larger applications, it often led to excessive boilerplate and fragmented logic.
Pinia keeps the same concepts but removes unnecessary ceremony:
No mutations
Simpler store structure
Direct state modification inside actions
Better TypeScript inference
First-class Composition API support
Pinia is now the officially recommended state management library for Vue 3.
Installing and Registering Pinia
First, install Pinia:
npm install pinia
Then register it in your application.
main.ts
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')
Once registered, any component can access stores.
Vuex Store Example
Let’s start with a typical Vuex store for a simple counter.
store/index.js (Vuex)
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0
},
getters: {
doubleCount(state) {
return state.count * 2
}
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
Using this in a component:
<script setup>
import { useStore } from 'vuex'
const store = useStore()
function increase() {
store.commit('increment')
}
</script>
Even in this small example, logic is split across mutations and actions, which often leads to context switching while reading the code.
The Same Store in Pinia
Now let’s write the same store using Pinia.
stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
})
Notice what changed:
No mutations
State is updated directly
Actions contain all logic
Everything lives in one place.
Using a Pinia Store in a Component
Accessing the store is straightforward.
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
function increase() {
counter.increment()
}
</script>
<template>
<div>
<p>{{ counter.count }}</p>
<button @click="increase">Increase</button>
</div>
</template>
Pinia stores behave like reactive objects, so they integrate naturally with the Composition API.
Composition API Integration
One advantage of Pinia is how well it works with composables.
You can access stores inside composables without additional wiring.
composables/useCart.ts
import { useCartStore } from '@/stores/cart'
export function useCart() {
const cart = useCartStore()
function addProduct(product) {
cart.items.push(product)
}
return {
items: cart.items,
addProduct
}
}
This pattern helps separate business logic from UI components, much like composables.
Set up Stores (Alternative Syntax)
Pinia also supports a setup-style store, which feels closer to Vue’s Composition API.
stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubleCount,
increment
}
})
This approach allows you to:
Use refs and computed directly
Reuse composables inside stores
Write logic using Composition API patterns
Devtools and Debugging
Pinia integrates with the Vue Devtools automatically.
You can inspect:
Store state
Getter values
Action calls
State changes over time
This makes debugging application state much easier, especially in large applications.
Migrating Gradually
A full rewrite is rarely necessary.
You can migrate incrementally.
Vue applications can run Vuex and Pinia at the same time:
app.use(store) // Vuex
app.use(pinia) // Pinia
A practical migration strategy:
Keep existing Vuex stores
Create new features using Pinia
Gradually migrate old modules
This reduces risk and keeps deployments stable.
When Migration Makes Sense
Moving to Pinia is most useful when:
Your Vuex store has grown complex
You want better TypeScript support
You are adopting the Composition API
You want simpler state logic
For new Vue 3 projects, Pinia is generally the better default.
Final Thoughts
Pinia doesn’t change the idea of a centralised state.
It simply removes the friction around it.
The result is state management code that is:
Easier to read
Easier to scale
Easier to maintain
For developers coming from Vue 2, switching from Vuex to Pinia often feels less like learning something new and more like removing unnecessary complexity.
In the next article, we’ll explore Vue 3 Tooling: Understanding Vite and the New Developer Experience.



