Migrating from Vuex to Pinia is mostly mechanical: each Vuex module becomes its own defineStore, mutations disappear, and string-based commit/dispatch calls become direct method calls. The hard part isn't the store itself. It's finding and updating every component that uses it.
Step-by-step conversion
Before: Vuex module
ts
const cartModule = {
namespaced: true,
state: () => ({
items: [] as CartItem[],
total: 0
}),
getters: {
itemCount: (state) => state.items.length
},
mutations: {
ADD_ITEM(state, item: CartItem) {
state.items.push(item)
state.total += item.price
},
CLEAR(state) {
state.items = []
state.total = 0
}
},
actions: {
async checkout({ state, commit }) {
await api.checkout(state.items)
commit('CLEAR')
}
}
}After: Pinia store
ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
)
const itemCount = computed(() => items.value.length)
function addItem(item: CartItem) {
items.value.push(item)
}
function clear() {
items.value = []
}
async function checkout() {
await api.checkout(items.value)
clear()
}
return { items, total, itemCount, addItem, clear, checkout }
})What changed
| Vuex | Pinia | Why |
|---|---|---|
state.total (manual tracking) | computed(() => ...) | Computed derives from state, no manual sync needed |
Mutations (ADD_ITEM, CLEAR) | Regular functions | Pinia tracks state changes through reactivity, no mutations layer needed |
commit('CLEAR') | clear() | Direct function call, type-safe |
state, commit destructured from context | Direct access to refs | Everything is in scope, no context object |
Namespaced module (cart/ADD_ITEM) | Independent store | No namespace strings, just import the store |
Updating components
vue
Open in Vue Playground<!-- Before: Vuex -->
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters('cart', ['itemCount'])
},
methods: {
...mapActions('cart', ['checkout'])
}
}
</script>
<!-- After: Pinia -->
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
<span>{{ cart.itemCount }} items</span>
<button @click="cart.checkout()">Checkout</button>
</template>Migration strategy for large apps
Don't rewrite everything at once. Pinia and Vuex can coexist in the same app:
- Install Pinia alongside Vuex
- Migrate one module at a time, starting with the simplest
- Update all components that use that module
- Remove the Vuex module
- Repeat until no Vuex modules remain
- Uninstall Vuex
See also: What is Pinia and how does it differ from Vuex? · How does Vuex work? · How does Pinia work internally?
References
- Migrating from Vuex - Pinia docs
- Defining a Store - Pinia docs
- Setup Stores - Pinia docs