En esta página
Pinia — gestión de estado global
¿Qué es Pinia?
Pinia es la biblioteca oficial de gestión de estado para Vue 3. Es el sucesor de Vuex y fue creada por Eduardo San Martín Morote, miembro del equipo core de Vue. Pinia está basada completamente en la Composition API y ofrece:
- TypeScript nativo: Tipos inferidos sin decoradores ni configuración adicional
- DevTools: Integración completa con Vue DevTools
- Modular: Cada store es independiente, sin módulos anidados
- Ligera: ~1KB (gzipped)
- SSR: Soporte completo para renderizado en servidor
Instalar y configurar Pinia
npm install pinia// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // Registrar Pinia antes de montar
app.mount('#app')defineStore() — crear un store
Cada store se define con defineStore(). El primer argumento es un ID único (el nombre del store):
import { defineStore } from 'pinia'
export const useProductosStore = defineStore('productos', /* ... */)
// ↑ Convención: 'use' + nombre en PascalCase + 'Store'
// ↑ ID único del storeSetup Stores — la sintaxis recomendada
Los Setup Stores usan la misma sintaxis que script setup:
// stores/productos.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface Producto {
id: number
nombre: string
precio: number
stock: number
}
export const useProductosStore = defineStore('productos', () => {
// STATE — refs y reactives
const productos = ref<Producto[]>([])
const filtro = ref('')
const cargando = ref(false)
// GETTERS — computed
const productosFiltrados = computed(() =>
productos.value.filter(p =>
p.nombre.toLowerCase().includes(filtro.value.toLowerCase())
)
)
const totalProductos = computed(() => productos.value.length)
const hayStock = computed(() =>
(id: number) => productos.value.find(p => p.id === id)?.stock ?? 0
)
// ACTIONS — funciones async o síncronas
async function cargarProductos(): Promise<void> {
cargando.value = true
try {
const res = await fetch('/api/productos')
productos.value = await res.json() as Producto[]
} finally {
cargando.value = false
}
}
function agregarProducto(producto: Omit<Producto, 'id'>): void {
const id = Math.max(0, ...productos.value.map(p => p.id)) + 1
productos.value.push({ id, ...producto })
}
function eliminarProducto(id: number): void {
productos.value = productos.value.filter(p => p.id !== id)
}
return {
productos,
filtro,
cargando,
productosFiltrados,
totalProductos,
hayStock,
cargarProductos,
agregarProducto,
eliminarProducto,
}
})Usar el store en componentes
<script setup lang="ts">
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useProductosStore } from '@/stores/productos'
const store = useProductosStore()
// storeToRefs para estado y getters (mantiene reactividad)
const { productos, filtro, cargando, productosFiltrados } = storeToRefs(store)
// Actions directamente del store (son funciones normales, no pierden contexto)
const { cargarProductos, eliminarProducto } = store
onMounted(cargarProductos)
</script>
<template>
<div>
<input v-model="filtro" placeholder="Filtrar productos..." />
<p v-if="cargando">Cargando...</p>
<ul v-else>
<li v-for="p in productosFiltrados" :key="p.id">
{{ p.nombre }} — ${{ p.precio }}
<button type="button" @click="eliminarProducto(p.id)">Eliminar</button>
</li>
</ul>
</div>
</template>Plugins de Pinia — persistencia
Un plugin muy común es pinia-plugin-persistedstate para guardar el estado en localStorage:
npm install pinia-plugin-persistedstate// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)// En el store
export const useCarritoStore = defineStore('carrito', () => {
const items = ref<ItemCarrito[]>([])
return { items }
}, {
persist: true, // Guardar en localStorage automáticamente
})
// O con configuración detallada
export const usePreferenciasStore = defineStore('preferencias', () => {
const tema = ref<'claro' | 'oscuro'>('claro')
const idioma = ref('es')
return { tema, idioma }
}, {
persist: {
storage: localStorage,
pick: ['tema'], // Solo persistir el tema
}
})Comunicación entre stores
Los stores de Pinia pueden usar otros stores directamente:
// stores/pedidos.ts
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'
import { useCarritoStore } from './carrito'
export const usePedidosStore = defineStore('pedidos', () => {
const auth = useAuthStore()
const carrito = useCarritoStore()
async function realizarPedido(): Promise<void> {
if (!auth.estaAutenticado) throw new Error('Debes iniciar sesión')
await fetch('/api/pedidos', {
method: 'POST',
body: JSON.stringify({
usuarioId: auth.usuario?.id,
items: carrito.items,
total: carrito.total,
}),
})
carrito.vaciar()
}
return { realizarPedido }
})$reset() — restaurar el estado inicial
Los Options Stores incluyen $reset() automáticamente. En Setup Stores, debes implementarlo manualmente:
export const useFormStore = defineStore('form', () => {
const nombre = ref('')
const email = ref('')
const mensaje = ref('')
function $reset(): void {
nombre.value = ''
email.value = ''
mensaje.value = ''
}
return { nombre, email, mensaje, $reset }
})$subscribe — observar cambios del store
const store = useCarritoStore()
// Se ejecuta cada vez que el estado del store cambia
store.$subscribe((mutacion, estado) => {
console.log('Store cambió:', mutacion.type, estado)
localStorage.setItem('carrito', JSON.stringify(estado))
})$patch — actualizar múltiples propiedades
const store = useCarritoStore()
// Actualizar múltiples propiedades en una sola transacción
store.$patch({
descuento: 0.15,
codigoPromo: 'VERANO25',
})
// Con función (para lógica más compleja)
store.$patch((state) => {
state.items.forEach(item => {
if (item.categoria === 'electronica') item.precio *= 0.9
})
})Práctica
- Store de tareas: Crea un
useTareasStorecon Setup Store. Incluye estado para la lista de tareas y el filtro activo. Implementa getters para tareas filtradas y conteo. Añade persistencia conpinia-plugin-persistedstate. - Store de autenticación: Implementa
useAuthStorecon las accioneslogin()ylogout(). Integra con el guard global de Vue Router para proteger rutas. UsastoreToRefscorrectamente en los componentes. - Comunicación entre stores: Crea un store de notificaciones
useNotificacionesStoreque otros stores puedan usar. EluseAuthStorellama anotificaciones.push('Bienvenido')tras un login exitoso.
En la siguiente lección profundizaremos en formularios y v-model para construir formularios reactivos.
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface Usuario {
id: number
nombre: string
email: string
rol: 'admin' | 'usuario'
}
// Setup store — sintaxis de Composition API (recomendada)
export const useAuthStore = defineStore('auth', () => {
// state — refs y reactives
const usuario = ref<Usuario | null>(null)
const token = ref<string | null>(null)
const cargando = ref(false)
// getters — computed
const estaAutenticado = computed(() => usuario.value !== null)
const esAdmin = computed(() => usuario.value?.rol === 'admin')
const nombreUsuario = computed(() => usuario.value?.nombre ?? 'Invitado')
// actions — funciones
async function login(email: string, password: string): Promise<void> {
cargando.value = true
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (!res.ok) throw new Error('Credenciales incorrectas')
const datos = await res.json() as { usuario: Usuario; token: string }
usuario.value = datos.usuario
token.value = datos.token
} finally {
cargando.value = false
}
}
function logout(): void {
usuario.value = null
token.value = null
}
return {
// Exponer estado, getters y acciones
usuario,
token,
cargando,
estaAutenticado,
esAdmin,
nombreUsuario,
login,
logout,
}
})
import { defineStore } from 'pinia'
interface ItemCarrito {
id: number
nombre: string
precio: number
cantidad: number
}
// Options store — sintaxis similar a Options API de Vue
export const useCarritoStore = defineStore('carrito', {
state: () => ({
items: [] as ItemCarrito[],
descuento: 0,
}),
getters: {
total: (state) =>
state.items.reduce((sum, i) => sum + i.precio * i.cantidad, 0),
totalConDescuento(): number {
return this.total * (1 - this.descuento)
},
cantidadItems: (state) =>
state.items.reduce((sum, i) => sum + i.cantidad, 0),
},
actions: {
agregarItem(item: Omit<ItemCarrito, 'cantidad'>): void {
const existente = this.items.find(i => i.id === item.id)
if (existente) {
existente.cantidad++
} else {
this.items.push({ ...item, cantidad: 1 })
}
},
eliminarItem(id: number): void {
this.items = this.items.filter(i => i.id !== id)
},
vaciar(): void {
this.items = []
this.descuento = 0
},
},
})
Inicia sesión para guardar tu progreso