Hook — 05 : 57 a.m., Medellín ↔ Tulum refactor sprint
A power‑flicker rattled my Colombian apartment just as Sofía, coding from a beachfront hostel in Tulum, DM’d: “Our Vuex boilerplate doubled since we moved to Vue 3, and TypeScript is screaming.” Minutes later we spiked the same feature in Pinia. Setup shrank from 120 lines to 35, hot‑module reload worked again, and the surf forecast widget stayed buttery smooth. Sofía merged the branch, packed her board, and paddled out before the sun got cruel. That save‑the‑swell moment frames today’s deep dive: choosing between Vuex and Pinia for modern Vue.js apps.
Why This Comparison Matters in 2025
Vue 3 ships with the Composition API, script‑setup sugar, and Vite’s lightning builds—but many tutorials still default to Vuex 4, a pattern born in 2015. Meanwhile, Pinia has matured into Vue’s official state‑management library, offering smaller bundles, simpler dev‑tools, and first‑class TypeScript. Picking the right store affects:
- Developer velocity—less boilerplate means faster features.
- Bundle weight—kilobytes matter on shaky 3 G connections from Costa Rica to Colombia.
- Type safety—critical for scaling remote teams across time zones.
- Ecosystem longevity—Vue 4 roadmaps target Pinia first.
Bird’s‑Eye View at a Glance
Feature | Pinia | Vuex 4 |
---|---|---|
API style | Composition & Options | Options |
Boilerplate | Low (no mutations) | Higher (state + getters + actions + mutations) |
TypeScript DX | Excellent (auto‑infer) | Manual typing or plugins |
DevTools | Timeline, hot‑reload by default | Supported but mutation noise |
Persist Plugins | PiniaPersist, localForage, etc. | vuex‑persistedstate |
SSR Support | Built‑in (ssr:true ) | Yes, manual hydration |
Vue 2 support | Via plugin | Native |
Bundle size (brotli) | ~1.5 kB | ~6 kB |
Step‑by‑Step Walkthroughs
1 – Declaring State
Pinia
tsCopyEdit// stores/useCart.ts
import { defineStore } from 'pinia';
export const useCart = defineStore('cart', {
state: () => ({ items: [] as { id:number; qty:number }[] }),
getters: {
total: (s) => s.items.reduce((t, i) => t + i.qty, 0),
},
actions: {
add(id:number) { this.items.push({ id, qty:1 }); },
},
});
Vuex 4
// store/index.ts
import { createStore } from 'vuex';
export default createStore({
state: () => ({ items: [] }),
getters: {
total: (state) => state.items.reduce((t,i) => t+i.qty,0),
},
mutations: {
ADD(state, id:number) { state.items.push({ id, qty:1 }); },
},
actions: {
add({ commit }, id:number) { commit('ADD', id); },
},
});
Pinia skips mutations; actions mutate state directly—no switch‑case ceremony.
2 – Consuming in Components
<script setup lang="ts">
import { useCart } from '@/stores/useCart';
const cart = useCart();
</script>
<template>
<button @click="cart.add(7)">Add to cart</button>
<span>{{ cart.total }} items</span>
</template>
Reactive store properties work like component refs—no mapGetters
helpers needed.
3 – Persisting State
Pinia plugin
import { createPinia } from 'pinia';
import piniaPersist from 'pinia-plugin-persistedstate';
const pinia = createPinia().use(piniaPersist);
app.use(pinia);
Add persist: true
to any store definition—done.
Vuex requires configuring vuex‑persistedstate and manually whitelisting modules.
4 – Lazy Loading for Code Splitting
With Pinia you can import a store only inside routes that need it, letting Vite tree‑shake unused code. Vuex modules live in a global registry; dynamic modules are possible but verbose.
Remote‑Work Insight Box
My team in Brazil hot‑reloads features via Vite’s dev server, but huge Vuex mutation logs slowed DevTools timeline to a crawl. Migrating one module to Pinia restored instant HMR—saving at least two coffee refills per day.
Performance & Accessibility Checkpoints
- Initial JS payload—Switching to Pinia shaved ~4.5 kB brotli from our lighthouse cold run.
- Time to Interactive—Fewer mutation proxies reduce Vue’s reactive overhead on low‑power devices.
- Dev build speed—Pinia’s ES modules compile faster; remote teams on cloud IDEs feel the difference.
- Accessibility—State libraries don’t touch a11y directly, but Pinia’s simpler patterns mean fewer missed updates that could desync ARIA live regions.
Common Pitfalls & How to Dodge Them
Issue | Pinia Fix | Vuex Fix |
---|---|---|
“Store is undefined” | Call useX() inside setup() after pinia plugin mounted | Register module before accessing |
Object loses reactivity | Use set() or Vue’s reactive ? | Same, but mutation must commit deep copy |
DevTools flood | Pinia shows only actions | Filter mutation logs |
SSR hydration mismatch | defineStore({ ssr: true }) | Manual store.replaceState() |
CLI Commands Worth Remembering
Command | Purpose |
---|---|
npm i pinia | Add Pinia |
vue add pinia | Vue‑CLI plugin (Vue 2/3) |
npx vite build --report | Inspect bundle sizes |
npx pinia-inspect | Stand‑alone dev tools if Vue DevTools blocked |
Diagram (text)
Component → calls useCart()
→ Pinia store proxy → state, getters, actions → Vue DevTools timeline (actions only). Parallel path shows Vuex dispatch → commit → mutation logs.
Wrap‑Up
- Choose Pinia when starting fresh on Vue 3, needing top‑tier TypeScript, or chasing lean bundles for users on prepaid data plans in Latin America.
- Stay on Vuex if you maintain a mature Vue 2 codebase, rely on Vuex ecosystem plugins, or need granular mutation history for auditors.
- Migration can be incremental—Pinia and Vuex coexist during transition.
Have migration war stories or doubts? Drop them below; I’ll respond between flights and late‑night arepa sessions.
Meta: Compare Pinia and Vuex for Vue.js—features, DX, and performance insights to help teams pick the right store library for modern apps.