Frontend state management: Pinia in the post-Vuex era
How Pinia replaced Vuex in the Vue ecosystem: less boilerplate, better TypeScript support, and a more mature approach to state management.
In the Vue ecosystem, state management was Vuex’s domain for a long time. Vuex got the job done — but when Vue 3 and the Composition API arrived, some rough edges became more visible. There was too much boilerplate, TypeScript support had been bolted on after the fact, and organizing a store around the mutation-action-getter triple often made simple problems feel unnecessarily complex.
Pinia emerged from Vue’s own team as the official successor to Vuex. It was designed for Vue 3, built to work naturally with the Composition API, and shipped with first-class TypeScript support from day one. Vuex will be removed entirely with Vue 5; Pinia is the officially recommended path right now.
Writing a store with Pinia
The most striking difference with Pinia is just how concise a store definition looks. Instead of the four-layer state/mutations/actions/getters structure from Vuex, you have a single defineStore call:
// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
function addItem(product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(productId) {
items.value = items.value.filter(i => i.id !== productId)
}
return { items, totalPrice, addItem, removeItem }
})
This is a store written exactly like a Composition API setup function. ref and computed are things you already know — there is no separate API to learn.
Using it in a component:
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
// Direct access
console.log(cart.totalPrice)
cart.addItem(product)
Comparison with Vuex
Doing the same thing in Vuex required at least four layers: defining state, writing a mutation, writing an action, defining a getter. Then in the component you’d wire everything up with mapState, mapActions, and mapGetters.
Pinia reduces this to three: state (ref), computed, and function. And all three are the standard building blocks of Vue’s Composition API.
On the TypeScript front, Vuex couldn’t infer types for accesses like $store.state.cart.items — manual type declarations were required. With Pinia, the moment you define your store with defineStore, every type is inferred automatically. The object returned by useCartStore() is fully typed.
Multiple stores
Cross-module access in Vuex could get confusing at times: string-based dispatches like this.$store.dispatch('auth/logout'). In Pinia, stores are independent of each other; if you want to use one store inside another, you simply import it.
// stores/order.js
import { useCartStore } from './cart'
import { useAuthStore } from './auth'
export const useOrderStore = defineStore('order', () => {
async function placeOrder() {
const cart = useCartStore()
const auth = useAuthStore()
// cart.items and auth.user are directly accessible
// ...
}
return { placeOrder }
})
No string references, no module hierarchy, just plain JavaScript.
$reset() and resetting store state
A feature that was missing in Vuex is available as an option in Pinia: $reset(). In stores written with the Options API style, it resets state back to its initial values. It comes in handy when logging out (clearing all stores) or cleaning up between steps in a form wizard.
When you use the setup style (the () => {} form I showed above), $reset() does not come out of the box — you have to write your own reset function. That is a small trade-off: you gain the flexibility of setup style, but give up the convenience of automatic $reset().
When you do not need state management
No matter how lean Pinia is, state management is not the right tool for every problem. If only a single component uses a piece of data, that component’s own refs are sufficient. If data is shared between two components, a props-and-emit cycle is usually cleaner.
A store makes the most sense in one specific scenario: when data needs to be read or mutated by multiple components that have no direct relationship with each other. A shopping cart, authentication state, global notifications — those are good candidates. The form state of a single page component — probably not.
Where I stand with Pinia
Migrating to Pinia turned out to be one of the smoothest transitions developers went through with Vue 3. The learning curve is nearly flat; if you know the Composition API, you understand Pinia at first glance. The reduction in boilerplate is substantial — especially noticeable in mid-sized stores.
It is encouraging to see the Vue ecosystem maturing. Vuex was not wrong; it was the right solution for its time. Pinia fits more naturally into Vue’s current philosophy.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.