
Pinia es la librería de gestión del estado recomendada para Vue 3, Vuex está en modo mantenimiento. Un store se declara con defineStore, se usa llamando a usePanierStore() desde cualquier componente y se desestructura con storeToRefs para no perder la reactividad.
Hacer subir un evento de un componente a su padre funciona bien en uno o dos niveles. Más allá, te pasas el rato reemitiendo eventos y bajando props a través de componentes que no hacen nada con ellas. Pinia resuelve ese problema: un estado declarado una sola vez, legible y modificable desde cualquier componente.
¿Pinia o Vuex?
La pregunta ya no se plantea. Vuex está en modo mantenimiento y la documentación oficial de Vue remite a Pinia, que se ha convertido en la librería de gestión del estado recomendada. Para un proyecto nuevo, elige Pinia sin dudarlo. Para un proyecto con Vuex que funciona, la migración no corre prisa: se hace store a store y ambos conviven.
Lo que Pinia cambia en la práctica: se acabaron las mutaciones, solo hay acciones, se acabaron los módulos anidados, cada store es un módulo, y un tipado que funciona sin contorsiones.
Instalar
npm install piniaDespués conecta el plugin a la aplicación:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')En un proyecto nuevo, el generador oficial se encarga: responde «sí» a Pinia durante npm create vue@latest.
Escribir un store
Un store se declara con defineStore. El primer argumento es un identificador único que usan las herramientas de desarrollo. El segundo admite dos formas, la forma setup es la que genera el utillaje oficial y la que más se parece a un componente.
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const usePanierStore = defineStore('panier', () => {
// estado: refs
const lignes = ref([])
// getters: computed
const total = computed(() =>
lignes.value.reduce((somme, l) => somme + l.prix * l.quantite, 0)
)
const nombreArticles = computed(() =>
lignes.value.reduce((somme, l) => somme + l.quantite, 0)
)
// acciones: funciones
function ajouter(produit) {
const ligne = lignes.value.find((l) => l.id === produit.id)
if (ligne) {
ligne.quantite += 1
return
}
lignes.value.push({ ...produit, quantite: 1 })
}
function retirer(id) {
lignes.value = lignes.value.filter((l) => l.id !== id)
}
function vider() {
lignes.value = []
}
return { lignes, total, nombreArticles, ajouter, retirer, vider }
})Todo lo que la función devuelve pasa a ser público. Lo que no devuelve queda privado dentro del store: así es como se guarda ahí un contador interno o una función de utilidad.
La convención de nombres es useQuelqueChoseStore, como en un composable.
La forma options
La otra escritura retoma la estructura de Vuex, sin las mutaciones:
export const usePanierStore = defineStore('panier', {
state: () => ({ lignes: [] }),
getters: {
total: (state) => state.lignes.reduce((s, l) => s + l.prix * l.quantite, 0)
},
actions: {
ajouter(produit) {
this.lignes.push({ ...produit, quantite: 1 })
}
}
})Las dos formas son equivalentes e intercambiables. La forma options facilita la llegada desde Vuex, la forma setup da más libertad, sobre todo para usar un composable dentro del store.
Usar el store
Llama a la función dentro del componente. La instancia es única: dos componentes que llaman a usePanierStore() obtienen exactamente el mismo objeto.
<script setup>
import { usePanierStore } from '@/stores/panier'
const panier = usePanierStore()
</script>
<template>
<p>{{ panier.nombreArticles }} article(s) — {{ panier.total }} €</p>
<ul>
<li v-for="ligne in panier.lignes" :key="ligne.id">
{{ ligne.nom }} × {{ ligne.quantite }}
<button @click="panier.retirer(ligne.id)">Retirer</button>
</li>
</ul>
<button @click="panier.vider()">Vider</button>
</template>Estado, getters y acciones se alcanzan directamente sobre el objeto, sin ningún intermediario.
storeToRefs, o la reactividad perdida
Es el error más frecuente con Pinia. Desestructurar el store da valores congelados:
const panier = usePanierStore()
const { total } = panier // ✗ número congelado, ya no cambiará
panier.ajouter({ id: 1, prix: 7 })
console.log(total) // 0storeToRefs extrae el estado y los getters conservando la reactividad:
import { storeToRefs } from 'pinia'
const panier = usePanierStore()
const { total, lignes } = storeToRefs(panier) // ✓ refs
const { ajouter, retirer } = panier // ✓ las acciones sí se desestructuran
panier.ajouter({ id: 1, prix: 7 })
console.log(total.value) // 7Quédate con la regla: el estado y los getters pasan por storeToRefs, las acciones se desestructuran directamente. Las acciones son simples funciones ligadas al store, no pierden nada.
Modificar el estado
Tres maneras, por orden de preferencia.
Una acción para toda la lógica de negocio. Es el caso normal: la regla se escribe una sola vez, en el store.
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })$patch para modificar varias entradas en una sola operación, lo que genera una única entrada en las herramientas de desarrollo:
panier.$patch({ lignes: [], codePromo: null })
// forma función, para los arrays
panier.$patch((state) => {
state.lignes.push(nouvelleLigne)
state.derniereMaj = Date.now()
})La asignación directa está permitida, al contrario que en Vuex, pero resérvala para los casos triviales:
panier.codePromo = 'RENTREE'En un store escrito en forma options, panier.$reset() restaura el estado inicial. En un store en forma setup, Pinia no conoce ese estado inicial: la llamada lanza un error. Escribe tu propia acción de reinicio, como la función vider() de más arriba.
Observar los cambios
$subscribe se llama en cada mutación del estado. Es el hook para persistir el carrito:
panier.$subscribe((mutation, state) => {
localStorage.setItem('panier', JSON.stringify(state.lignes))
})Si la necesidad de persistencia es más amplia, un plugin de Pinia hace el trabajo en todos los stores. Un plugin es una función que recibe el contexto y enriquece cada store:
export function persistance({ store }) {
const sauvegarde = localStorage.getItem(store.$id)
if (sauvegarde) {
store.$patch(JSON.parse(sauvegarde))
}
store.$subscribe((_, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}const pinia = createPinia()
pinia.use(persistance)Probar un store
Un store se prueba sin montar ningún componente. Solo hace falta activar una instancia de Pinia nueva antes de cada test, para partir de un estado limpio:
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { usePanierStore } from '../panier.js'
describe('store panier', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('cumule les quantités du même produit', () => {
const panier = usePanierStore()
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })
expect(panier.lignes).toHaveLength(1)
expect(panier.nombreArticles).toBe(2)
expect(panier.total).toBe(50)
})
})Sin setActivePinia, la llamada al store falla con el mensaje « «getActivePinia()» was called but there was no active Pinia. Are you trying to use a store before calling «app.use(pinia)»? ».
Cuándo no usar Pinia
Un store no sale gratis: añade un archivo, una indirección y un estado global más. Se justifica cuando un dato lo usan componentes alejados entre sí, o cuando tiene que sobrevivir al desmontaje de un componente.
Para datos que vienen de un servicio en tiempo real, mira mejor Firebase y VueFire, cuyos enlaces ya son reactivos.
Para el estado local de un formulario, basta un ref. Para un estado compartido entre un padre y su hijo directo, las props y los eventos hacen el trabajo. Para cachear datos del servidor, una herramienta dedicada como TanStack Query responde mejor a la necesidad.
Si acabas de descubrir Vue, empieza por el tutorial de Vue 3 para principiantes: Pinia cobra todo su sentido una vez asimilados los componentes y los eventos.
Errores frecuentes
const { total } = panier da un valor congelado. Pasa por storeToRefs(panier) para el estado y los getters, las acciones, en cambio, se desestructuran directamente.n$reset en un store setup | Pinia no conoce el estado inicial de un store escrito en forma setup: la llamada lanza un error. Escribe tu propia acción de reinicio.setActivePinia(createPinia()) antes de cada caso.

