Pinia vs Vuex in 2026: Vue State Management Evolution
·PkgPulse Team
TL;DR
Pinia is the official Vue state management library. Vuex is in maintenance mode. Pinia (~2.5M weekly downloads) is lighter, has better TypeScript support, and drops Vuex's mutations in favor of direct state changes. Vuex (~2.8M downloads) is still widely used in legacy Vue 2/3 apps. For any new Vue 3 project, use Pinia. The Vuex → Pinia migration is well-documented and usually straightforward.
Key Takeaways
- Pinia: ~2.5M weekly downloads — Vuex: ~2.8M (much is legacy)
- Pinia is Vue's official recommendation — Evan You endorsed it, Vuex 5 was cancelled
- Pinia drops mutations — state changes directly in actions
- Pinia has first-class TypeScript — no need for complex generic gymnastics
- Pinia devtools work seamlessly — Vue DevTools extension supports both
Vuex: The Legacy Approach
// Vuex 4 — strict mutation/action separation
import { createStore } from 'vuex';
const store = createStore({
state: {
count: 0,
user: null,
},
getters: {
doubleCount: (state) => state.count * 2,
isLoggedIn: (state) => !!state.user,
},
mutations: {
// Must be synchronous — the only way to modify state
INCREMENT(state) { state.count++; },
SET_USER(state, user) { state.user = user; },
LOGOUT(state) { state.user = null; },
},
actions: {
// Can be async — commits mutations
async login({ commit }, credentials) {
const user = await api.login(credentials);
commit('SET_USER', user);
},
logout({ commit }) {
commit('LOGOUT');
},
},
});
<!-- Vuex usage in components -->
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const count = computed(() => store.state.count);
const doubleCount = computed(() => store.getters.doubleCount);
const increment = () => store.dispatch('increment'); // Dispatch actions
// Or: store.commit('INCREMENT') — direct mutation
</script>
Pinia: The Modern Approach
// Pinia — no mutations, simpler structure
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
// Actions can be sync or async — modify state directly
increment() { this.count++; },
async incrementAsync() {
await new Promise(r => setTimeout(r, 1000));
this.count++;
},
},
});
// Or use composition API style (preferred in Vue 3):
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() { count.value++; }
async function fetchInitialCount() {
count.value = await api.getCount();
}
return { count, doubleCount, increment, fetchInitialCount };
});
<!-- Pinia usage in components -->
<script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();
// storeToRefs preserves reactivity when destructuring
const { count, doubleCount } = storeToRefs(counterStore);
const { increment } = counterStore; // Actions don't need storeToRefs
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
TypeScript Support
// Vuex with TypeScript — complex generics required
// Need to define types for state, getters, mutations, actions separately
interface State {
count: number;
user: User | null;
}
// useStore() returns a generic Store<...> type — complex to work with
const store = useStore<State>();
// store.state.count — typed
// But getters and mutations often lose types without extensive setup
// Pinia with TypeScript — just works
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null);
const isLoggedIn = computed(() => !!user.value);
async function login(credentials: LoginCredentials) {
user.value = await api.login(credentials); // Fully typed
}
return { user, isLoggedIn, login };
});
// Usage — full type inference
const userStore = useUserStore();
userStore.user; // User | null — correctly typed
userStore.login({}) // TypeScript checks credentials shape
Migration from Vuex to Pinia
// Vuex module → Pinia store (1:1 mapping)
// BEFORE (Vuex module):
const cartModule = {
namespaced: true,
state: { items: [] },
getters: { total: (state) => state.items.reduce(...) },
mutations: { ADD_ITEM(state, item) { state.items.push(item); } },
actions: { addItem({ commit }, item) { commit('ADD_ITEM', item); } },
};
// AFTER (Pinia store):
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: { total: (state) => state.items.reduce(...) },
actions: {
// Action + mutation combined
addItem(item) { this.items.push(item); },
},
});
Pinia Plugins and Devtools
// Pinia plugins — extend all stores
import { createPinia } from 'pinia';
const pinia = createPinia();
// Persist plugin (localStorage)
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
pinia.use(piniaPluginPersistedstate);
// Per-store persistence:
export const useSettingsStore = defineStore('settings', {
state: () => ({ theme: 'light', language: 'en' }),
persist: true, // Automatically persists to localStorage
});
When to Choose
Choose Pinia when:
- Building any new Vue 3 application
- TypeScript is important
- You want simpler, more maintainable stores
- Vue 2 + Pinia via bridge package (if upgrading)
Keep Vuex when:
- Existing Vue 2 project with large Vuex codebase
- Migration cost isn't justified currently
- Vue 2 compatibility is required
The trajectory is clear: Pinia is Vue's state management future. Vuex is maintenance-only.
Compare Pinia and Vuex package health on PkgPulse.
See the live comparison
View pinia vs. vuex on PkgPulse →