¿Qué es Pinia?
Pinia es la librería oficial de gestión de estado para Vue. Sustituyó a Vuex como el store recomendado en Vue 3 y ahora es la opción predeterminada en los nuevos proyectos de Vue y Nuxt. Es ligera, type-safe y está construida directamente sobre la Composition API.
Su diseño es intencionalmente minimalista. Un store es una función que devuelve el estado, valores derivados y acciones. No existen las mutations, ni el namespacing de módulos, ni un provider por store. Si conoces ref, computed y las funciones, ya conoces la mayor parte de Pinia.
Definiendo un store
Un store se crea con defineStore, que recibe un id único y una función de configuración (setup function).
// stores/cart.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCartStore = defineStore("cart", () => {
const items = ref([]);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0),
);
const count = computed(() => items.value.length);
function add(item) {
items.value.push(item);
}
function remove(id) {
items.value = items.value.filter((item) => item.id !== id);
}
return { items, total, count, add, remove };
});
Este es un setup store: ref se convierte en el estado (state), computed se convierte en un getter y las funciones se convierten en acciones. El id único sirve para nombrar el store en las devtools y es obligatorio.
Pinia también soporta un options store si prefieres una estructura similar a la de Vuex:
// stores/counter.js
export const useCounter = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count += 1;
},
},
});
Ambas formas están totalmente soportadas. Los setup stores suelen encajar mejor con TypeScript y la Composition API, mientras que los options stores pueden resultar más familiares para los usuarios de Vuex.
Uso de un store en componentes
Llama a la función del store para obtener la instancia del store. No es necesario utilizar ningún provider para envolver tus componentes.
<!-- Cart.vue -->
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";
const cart = useCartStore();
const { items, total } = storeToRefs(cart);
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} — {{ item.price }}
<button @click="cart.remove(item.id)">Remove</button>
</li>
</ul>
<p>Total: {{ total }}</p>
</template>
La lectura de cart.total directamente en una plantilla se mantiene reactiva. Cuando quieras hacer destructuring del estado o de los getters manteniendo la reactividad, utiliza storeToRefs, que los convierte en refs. Las acciones pueden desestructurarse directamente ya que no requieren reactividad.
Getters
Los getters son valores computados derivados del estado. Se almacenan en caché y se comparten, por lo que cada componente que lee el mismo getter comparte un único cálculo.
// stores/products.js
export const useProductsStore = defineStore("products", () => {
const products = ref([]);
const filter = ref("");
const visible = computed(() =>
products.value.filter((p) =>
p.name.toLowerCase().includes(filter.value.toLowerCase()),
),
);
const inStock = computed(() =>
visible.value.filter((p) => p.stock > 0),
);
return { products, filter, visible, inStock };
});
Intentar usar watch para computar un valor es un error común. Si el valor puede derivarse del estado existente, debería ser un getter, exactamente igual a como usarías computed en un componente.
Acciones
Las acciones son funciones que modifican el estado. Pueden ser síncronas o asíncronas, y pueden llamar a otras acciones o incluso a otros stores.
// stores/users.js
export const useUsersStore = defineStore("users", () => {
const users = ref([]);
const loading = ref(false);
const error = ref(null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
users.value = await res.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
return { users, loading, error, fetchUsers };
});
Sin embargo, para los datos del servidor, considera utilizar una capa de obtención de datos como TanStack Query, que gestiona el almacenamiento en caché y la invalidación. Pinia es ideal para el estado del cliente: carritos, filtros, preferencias de UI, estado de autenticación y borradores.
Plugins y persistencia
Los plugins de Pinia se ejecutan para cada store y pueden añadir funcionalidades como persistencia, logging o reseteo.
// persist.js
export function persistPlugin({ store }) {
const saved = localStorage.getItem(store.$id);
if (saved) store.$patch(JSON.parse(saved));
store.$subscribe((_mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}
// main.js
const pinia = createPinia();
pinia.use(persistPlugin);
app.use(pinia);
El popular pinia-plugin-persistedstate hace lo mismo, ofreciendo opciones sobre qué claves almacenar. La persistencia es una de las razones principales para recurrir a un store en lugar del estado local de un componente.
Pinia con Nuxt
En Nuxt, instala el módulo oficial y los stores en stores/ se importarán automáticamente.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
});
<!-- pages/cart.vue -->
<script setup>
const cart = useCartStore(); // auto-imported
</script>
Debido a que Nuxt gestiona la instalación y el renderizado en el servidor, el mismo store funciona tanto en el servidor como en el cliente. Consulta la guía de Nuxt para ver cómo se integra esto con useFetch y las rutas de servidor.
Mejores prácticas
- Prefiere los setup stores para el código nuevo, especialmente con TypeScript.
- Mantén los datos derivados en getters en lugar de observar el estado.
- Usa
storeToRefsal desestructurar el estado o los getters. - Mantén los datos del servidor en una capa de obtención de datos (data-fetching), no en Pinia.
- Usa acciones para cualquier cosa que modifique el estado, incluyendo tareas asíncronas.
- Persiste únicamente el estado que deba sobrevivir a una recarga de página.
- Nombra los stores de forma clara según el dominio al que pertenezcan.
Errores comunes
- Desestructurar el estado sin
storeToRefsy perder la reactividad. - Usar
watchpara calcular un valor que debería derivarse mediante un getter. - Guardar datos obtenidos del servidor en el store y gestionar manualmente los flags de carga.
- Olvidar instalar Pinia, provocando que las llamadas al store fallen en tiempo de ejecución.
- Crear un único store enorme en lugar de stores enfocados en dominios específicos.
- Mutar el estado desde fuera de una acción de una manera que sea difícil de rastrear.
Próximos pasos
Pinia es el equivalente en Vue de un store pequeño y moderno. Profundiza tus conocimientos de Vue, añade Nuxt para el renderizado en el servidor y las auto-importaciones, y compara este enfoque con la Context API de React, Zustand y Redux Toolkit. Después, construye un store pequeño con un getter, una acción asíncrona y persistencia para ver la poca cantidad de código que requiere.