Skip to main content

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.