¿Qué es Pinia?
Pinia es la librería oficial de gestión de estado para Vue. Reemplaza a Vuex como solución recomendada gracias a una API más sencilla, soporte nativo de TypeScript y una arquitectura más moderna.
Las principales ventajas de Pinia frente a Vuex son:
- API simplificada: no existen mutations como concepto separado, las acciones modifican el estado directamente.
- Soporte TypeScript nativo: inferencia de tipos automática sin necesidad de configuraciones adicionales.
- Sin módulos anidados: cada store es independiente y plano, lo que facilita la organización.
- Integración con DevTools: inspección del estado, seguimiento de acciones y viaje en el tiempo (time-travel debugging).
- Extremadamente ligero: aproximadamente 1 KB comprimido.

Instalación
Para añadir Pinia a un proyecto Vue, se ejecuta:
npm install pinia
Configuración en main.ts
Una vez instalado, se debe crear la instancia de Pinia y registrarla como plugin de la aplicación Vue en el archivo main.ts:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
La función createPinia() crea una instancia raíz que alberga todos los stores. Al registrarla con app.use(pinia), todos los componentes de la aplicación pueden acceder a cualquier store.
Options Store con defineStore
La forma más directa de crear un store es mediante el patrón Options Store, que recuerda a la Options API de Vue. Se utiliza la función defineStore que recibe un identificador único y un objeto de configuración:
// src/stores/taskStore.ts
import { defineStore } from 'pinia'
interface Task {
id: number
title: string
completed: boolean
}
export const useTaskStore = defineStore('task', {
state: () => ({
tasks: [] as Task[],
isLoading: false,
filter: 'all' as 'all' | 'completed' | 'pending'
}),
getters: {
filteredTasks(state): Task[] {
if (state.filter === 'completed') {
return state.tasks.filter(t => t.completed)
}
if (state.filter === 'pending') {
return state.tasks.filter(t => !t.completed)
}
return state.tasks
},
completedCount(state): number {
return state.tasks.filter(t => t.completed).length
},
// Un getter puede usar otros getters a través de this
progressPercentage(): number {
if (this.tasks.length === 0) return 0
return Math.round((this.completedCount / this.tasks.length) * 100)
}
},
actions: {
addTask(title: string) {
const newTask: Task = {
id: Date.now(),
title,
completed: false
}
this.tasks.push(newTask)
},
toggleTask(id: number) {
const task = this.tasks.find(t => t.id === id)
if (task) {
task.completed = !task.completed
}
},
async fetchTasks() {
this.isLoading = true
try {
const response = await fetch('/api/tasks')
this.tasks = await response.json()
} finally {
this.isLoading = false
}
},
removeTask(id: number) {
this.tasks = this.tasks.filter(t => t.id !== id)
}
}
})
Los tres bloques del Options Store son:
- state: función que retorna un objeto con el estado inicial. Debe ser una función para que cada instancia tenga su propio estado.
- getters: propiedades computadas derivadas del estado. Reciben
statecomo primer argumento o acceden a otros getters mediantethis. - actions: métodos que modifican el estado. Pueden ser síncronos o asíncronos y acceden al estado con
this.
Uso del store en componentes
Para utilizar un store en un componente, se importa la función del store y se invoca dentro de <script setup>:
<script setup lang="ts">
import { useTaskStore } from '@/stores/taskStore'
const taskStore = useTaskStore()
</script>
<template>
<div>
<p>Total tareas: {{ taskStore.tasks.length }}</p>
<p>Completadas: {{ taskStore.completedCount }}</p>
<p>Progreso: {{ taskStore.progressPercentage }}%</p>
<ul>
<li v-for="task in taskStore.filteredTasks" :key="task.id">
<input
type="checkbox"
:checked="task.completed"
@change="taskStore.toggleTask(task.id)"
/>
<span :class="{ 'line-through': task.completed }">
{{ task.title }}
</span>
<button @click="taskStore.removeTask(task.id)">Eliminar</button>
</li>
</ul>
</div>
</template>
El store se instancia una sola vez; cada llamada a useTaskStore() devuelve la misma instancia reactiva. El estado, los getters y las acciones están disponibles directamente como propiedades del objeto retornado.
Acceso al estado, getters y acciones
Existen varias formas de acceder a las propiedades de un store:
<script setup lang="ts">
import { useTaskStore } from '@/stores/taskStore'
const taskStore = useTaskStore()
// Acceso directo al estado
console.log(taskStore.tasks)
console.log(taskStore.isLoading)
// Lectura de getters (se comportan como propiedades computadas)
console.log(taskStore.filteredTasks)
console.log(taskStore.completedCount)
// Llamada a acciones
taskStore.addTask('Estudiar Vue')
taskStore.toggleTask(1)
</script>
Es importante destacar que no se debe desestructurar el estado o los getters directamente, ya que se pierde la reactividad:
// Esto pierde reactividad
const { tasks, completedCount } = taskStore // NO reactivo
// Para desestructurar manteniendo reactividad se usa storeToRefs
import { storeToRefs } from 'pinia'
const { tasks, completedCount } = storeToRefs(taskStore) // Reactivo
Las acciones sí pueden desestructurarse sin problema, ya que son funciones simples:
const { addTask, toggleTask } = taskStore // Correcto
Reactividad del estado en templates
El estado de Pinia es completamente reactivo en los templates. Cuando el estado cambia, los componentes que lo utilizan se actualizan automáticamente:
<script setup lang="ts">
import { ref } from 'vue'
import { useTaskStore } from '@/stores/taskStore'
const taskStore = useTaskStore()
const newTaskTitle = ref('')
function handleAddTask() {
if (newTaskTitle.value.trim()) {
taskStore.addTask(newTaskTitle.value.trim())
newTaskTitle.value = ''
}
}
</script>
<template>
<div>
<h2>Gestor de Tareas</h2>
<form @submit.prevent="handleAddTask">
<input v-model="newTaskTitle" placeholder="Nueva tarea..." />
<button type="submit">Añadir</button>
</form>
<div>
<button @click="taskStore.filter = 'all'">Todas</button>
<button @click="taskStore.filter = 'completed'">Completadas</button>
<button @click="taskStore.filter = 'pending'">Pendientes</button>
</div>
<p v-if="taskStore.isLoading">Cargando...</p>
<ul v-else>
<li v-for="task in taskStore.filteredTasks" :key="task.id">
<input
type="checkbox"
:checked="task.completed"
@change="taskStore.toggleTask(task.id)"
/>
{{ task.title }}
</li>
</ul>
<footer>
{{ taskStore.completedCount }} de {{ taskStore.tasks.length }} completadas
({{ taskStore.progressPercentage }}%)
</footer>
</div>
</template>
Cualquier cambio en taskStore.tasks, taskStore.filter o cualquier otra propiedad del estado desencadena la actualización del componente de forma automática.
Buenas prácticas
- Un store por dominio: crear stores separados para cada área funcional (
useUserStore,useCartStore,useNotificationStore). - Convención de nombres: usar el prefijo
usey el sufijoStorepara identificar claramente los stores. - Ubicación de archivos: organizar los stores en
src/stores/con un archivo por store. - Lógica en acciones: mantener la lógica de negocio y las llamadas a API dentro de las acciones, no en los componentes.
- Getters para datos derivados: usar getters en lugar de computar valores en cada componente que necesite esos datos.
- Tipado explícito: definir interfaces para el estado y tipar los getters para aprovechar al máximo TypeScript.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje