Tutorial de introducción a Pinia
En Vue3, la gestión del estado es un tema inevitable.
Pinia es una biblioteca ligera de gestión de estado para Vue.js, que te permite compartir y gestionar estado entre componentes. Podemos imaginar Pinia como un almacén de datos global, donde todos los componentes pueden obtener o actualizar datos.
En este capítulo presentaremos la biblioteca de gestión de estado recomendada oficialmente para Vue3: Pinia. En comparación con Vuex, Pinia ofrece un esquema de gestión de estado más simple y más acorde con la filosofía de la Composition API de Vue3.
La siguiente imagen compara las diferencias estructurales entre ambos:
- Vuex utiliza una estructura de árbol con un solo store y módulos de varios niveles, jerarquía fija, depende de mutations, y es más pesado en general.
- Pinia, en cambio, se compone de múltiples stores independientes: es plano, ligero, sin división en módulos y sin la carga de los espacios de nombres.

Características principales de Pinia:
- Compatible con Vue2 y Vue3
- Diseño de API minimalista
- Soporte completo de TypeScript
- Compatible con Composition API
- Diseño modular, sin necesidad de módulos anidados
La estructura plana de Pinia permite que los componentes se conecten directamente a cualquier store y tomen lo que necesiten, sin pasar por módulos en árbol ni por rutas de espacios de nombres. El flujo de estado es simple y directo, con bajo acoplamiento.

Instalación y configuración
Crear un proyecto con Vite:
npm create vite@latest vue-pinia-demo --template vue cd vue-pinia-demo npm install
Instalar Pinia
Primero, instala Pinia en tu proyecto Vue3:
npm install pinia # 或者 yarn add pinia
Pinia tiene tres partes:
- state: almacena datos
- getters: calcula datos derivados
- actions: ejecutan lógica y modifican el state
Configurar Pinia
Configura Pinia en src/main.js o src/main.ts:
Ejemplo
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// Crear la instancia de Pinia
const pinia = createPinia()
// Crear la aplicación Vue
const app = createApp(App)
// Usar Pinia
app.use(pinia)
app.mount('#app')
Crear el primer Store
Un Store es el almacén de datos en Pinia. Vamos a crear un Store de contador simple.
Definir el Store
Crear un nuevo archivo src/stores/useCounter.js:
Ejemplo
import { defineStore } from 'pinia'
// Usar defineStore para definir el store
// El primer parámetro es el ID único del store
// El segundo parámetro son las opciones de configuración del store
export const useCounterStore = defineStore('counter', {
// state: define los datos de estado del store
state: () => ({
count: 0,
name: 'Mi contador'
}),
// getters: define propiedades calculadas basadas en state
getters: {
doubleCount: (state) => state.count * 2,
// Usar this para acceder a otros getters
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
// actions: define métodos para modificar state
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
// Puede recibir parámetros
incrementBy(amount) {
this.count += amount
},
// action asíncrono
async incrementAsync() {
// Simular una operación asíncrona
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
})
Explicación de la estructura del Store
Entendamos las partes del Store mediante una tabla:
| Parte | Función | Ejemplo |
|---|---|---|
| state | Define los datos almacenados | count: 0 |
| getters | Propiedades calculadas basadas en state | doubleCount: state => state.count * 2 |
| actions | Métodos que modifican el state | increment() { this.count++ } |
Usar el Store en un componente
Uso básico
El código del componente src/components/CounterComponent.vue es el siguiente:
Ejemplo
<template>
<div class="counter">
<h3>{{ store.name }}</h3>
<p>Contador actual: {{ store.count }}</p>
<p>Doble contador: {{ store.doubleCount }}</p>
<p>Doble más uno: {{ store.doubleCountPlusOne }}</p>
<button @click="store.increment()">+1</button>
<button @click="store.decrement()">-1</button>
<button @click="store.incrementBy(5)">+5</button>
<button @click="store.incrementAsync()">Asíncrono +1</button>
<button @click="reset">Restablecer</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/useCounter'
// En el setup usar el store
const store = useCounterStore()
// Método para restablecer el estado
function reset() {
store.$reset() // $reset puede restablecer el state a su valor inicial
}
</script>
Modifica el código de src/App.vue de la siguiente manera:
Ejemplo
import CounterComponent from './components/CounterComponent.vue'
</script>
<template>
<CounterComponent />
</template>
Estructura completa del proyecto:

Ejecutanpm run devel comando, y accede en el navegador ahttp://localhost:5173/, y observa el resultado:

Desestructuración reactiva
Si quieres usar directamente las propiedades de state en la plantilla, puedes usarstoreToRefs:
Ejemplo
<div>
<p>Contador: {{ count }}</p>
<p>Nombre: {{ name }}</p>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// Usar storeToRefs para mantener la reactividad
const { count, name } = storeToRefs(store)
// Nota: ¡desestructurar directamente perderá la reactividad!
// Forma incorrecta: const { count, name } = store
</script>
Store al estilo Composition API
Pinia también admite definir Stores con el estilo de la Composition API:
Ejemplo
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// state
const user = ref(null)
const isLoggedIn = ref(false)
// getters
const userName = computed(() => user.value?.name || 'Visitante')
const userAge = computed(() => user.value?.age || 0)
// actions
function login(userData) {
user.value = userData
isLoggedIn.value = true
}
function logout() {
user.value = null
isLoggedIn.value = false
}
return {
user,
isLoggedIn,
userName,
userAge,
login,
logout
}
})
Interacción entre Stores
Varios Stores pueden llamarse entre sí:
Ejemplo
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
addItem(product) {
this.items.push(product)
// Llamar a la action de otro store
const userStore = useUserStore()
if (userStore.isLoggedIn) {
// Sincronizar con el carrito del usuario
this.syncToUserCart()
}
}
}
})
Ejemplo práctico: carrito de compras
Creemos un ejemplo completo de carrito de compras:
Ejemplo
export const useProductStore = defineStore('products', {
state: () => ({
products: [
{ id: 1, name: 'Computadora portátil', price: 5999, stock: 10 },
{ id: 2, name: 'Teléfono inteligente', price: 3999, stock: 20 },
{ id: 3, name: 'Auriculares inalámbricos', price: 299, stock: 50 }
]
}),
getters: {
// Obtener el producto según su ID
getProductById: (state) => (id) => {
return state.products.find(product => product.id === id)
},
// Obtener los productos en stock
availableProducts: (state) => {
return state.products.filter(product => product.stock > 0)
}
}
})
Ejemplo
export const useCartStore = defineStore('cart', {
state: () => ({
items: [], // { productId, quantity }
discount: 0
}),
getters: {
// Número total de artículos en el carrito
totalItems: (state) => {
return state.items.reduce((total, item) => total + item.quantity, 0)
},
// Importe total del carrito
totalPrice: (state) => {
const productStore = useProductStore()
return state.items.reduce((total, item) => {
const product = productStore.getProductById(item.productId)
return total + (product?.price || 0) * item.quantity
}, 0)
},
// Precio total después del descuento
finalPrice: (state) => {
return state.totalPrice * (1 - state.discount / 100)
}
},
actions: {
// Agregar un producto al carrito
addToCart(productId, quantity = 1) {
const existingItem = this.items.find(item => item.productId === productId)
if (existingItem) {
existingItem.quantity += quantity
} else {
this.items.push({ productId, quantity })
}
},
// Eliminar un producto del carrito
removeFromCart(productId) {
this.items = this.items.filter(item => item.productId !== productId)
},
// Vaciar el carrito
clearCart() {
this.items = []
this.discount = 0
},
// Establecer el descuento
setDiscount(percent) {
this.discount = Math.max(0, Math.min(100, percent))
}
}
})
Usar el carrito de compras en un componente
Ejemplo
<template>
<div class="shopping-cart">
<h3>Carrito ({{ cart.totalItems }} artículos)</h3>
<div v-if="cart.items.length === 0">
<p>El carrito está vacío</p>
</div>
<div v-else>
<div v-for="item in cartItems" :key="item.product.id" class="cart-item">
<span>{{ item.product.name }}</span>
<span>¥{{ item.product.price }}</span>
<span>Cantidad: {{ item.quantity }}</span>
<span>Subtotal: ¥{{ item.product.price * item.quantity }}</span>
<button @click="cart.removeFromCart(item.product.id)">Eliminar</button>
</div>
<div class="cart-summary">
<p>Total: ¥{{ cart.totalPrice }}</p>
<p v-if="cart.discount >0">Descuento: {{ cart.discount }}%</p>
<p>Total a pagar: ¥{{ cart.finalPrice }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/products'
const cart = useCartStore()
const products = useProductStore()
// Calcular los detalles de los productos del carrito
const cartItems = computed(() => {
return cart.items.map(item => ({
product: products.getProductById(item.productId),
quantity: item.quantity
} }).filter(item => item.product) // Filtrar los productos que no existen
})
</script>
Mejores prácticas y consideraciones
1. Convenciones de nomenclatura del Store
Ejemplo
export const useUserStore = defineStore('user', { /* ... */ })
export const useProductStore = defineStore('products', { /* ... */ })
// Nombres a evitar
export const userStore = defineStore('user', { /* ... */ }) // Falta el prefijo use
2. Inicialización del estado
Ejemplo
state: () => ({
items: [],
loading: false,
error: null
})
// No recomendado: usar un objeto directamente
state: {
items: [] // ¡Esto hará que todas las instancias compartan el mismo array!
}
3. Manejo de operaciones asíncronas
Ejemplo
async fetchUserData(userId) {
this.loading = true
this.error = null
try {
const response = await api.getUser(userId)
this.user = response.data
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
}
}
4. Persistencia de datos
Para los datos que necesitan persistencia, se puede usar un plugin:
npm install pinia-plugin-persistedstate
Ejemplo
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: {}
}),
// Habilitar persistencia
persist: true
})
Pinia API
| Categoría | API | Descripción |
|---|---|---|
| Creación | defineStore |
Definir un store independiente que contenga state / getters / actions |
| Instanciación | createPinia |
Crear la instancia raíz de Pinia, para usarla en app.use() |
| Uso en componentes | useStore()(definido por el usuario, comouseUserStore) |
Llamar a un store, devuelve state, getters y actions reactivos |
| Estado | state |
Devuelve una función-objeto para definir el estado global mutable |
| Operaciones de estado | store.$state |
Leer y escribir directamente todo el state (a nivel de objeto) |
| Operaciones de estado | store.$patch() |
Modificar state en lote, admite dos modos: objeto y función |
| Reemplazo de estado | store.$reset() |
Restablecer al state inicial, solo disponible cuando se define con el método setup |
| Suscripción de estado | store.$subscribe() |
Escuchar cambios en state, adecuado para persistencia local |
| Llamada de actions | store.$onAction() |
Escuchar antes y después de la llamada a actions, se puede usar para registros y seguimiento |
| Getter | getters |
Datos derivados, caché automático basado en state |
| Plugins | pinia.use() |
Registrar plugins, ampliar las capacidades del store |
| Método de definición | defineStore(id, options) |
Sintaxis de Options API |
| Método de definición | defineStore(id, () => {...}) |
Sintaxis de Setup, devuelve state / getter / action |
| Propiedad del store | store.$id |
Identificador único del store actual |
| Propiedad del store | store.$ready(en algunas versiones) |
Indicador de estado después de que el store se inicializa |
| Persistencia (plugin) | persist |
Habilitar plugin de almacenamiento (como localStorage) |
| Funciones de utilidad | storeToRefs() |
Convertir state / getter en refs, manteniendo la reactividad |
Otras extensionesPara más contenido, consulte el sitio oficial de Pinia:https://pinia.vuejs.org/