Pinia: introducción y configuración

Intermedio
Vuejs
Vuejs
Actualizado: 27/03/2026

¿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.

Pinia: arquitectura de stores con state, getters y actions

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 state como primer argumento o acceden a otros getters mediante this.
  • 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 use y el sufijo Store para 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 - Autor del tutorial

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