Pinia — Gestión de estado global

En este capítulo aprenderás a usar Pinia para gestionar datos compartidos entre componentes y páginas, y a implementar la función de «guardar artículos como favoritos».


¿Por qué necesitamos gestión de estado global?

Hasta ahora, los datos se transmiten entre componentes mediante props y emit.

Esto es suficiente para escenarios con relaciones simples entre padre e hijo, pero se vuelve complicado cuando te encuentras con las siguientes situaciones:

  • El estado de favoritos debe sincronizarse entre la página de inicio y la página de detalles
  • El estado de inicio de sesión del usuario debe ser compartido por todas las páginas
  • Los datos del carrito de compras deben transferirse entre la barra de navegación, la página de productos y la página de pago

Transmisión de props por capasEl punto débil: si los datos del componente A deben transmitirse al componente D, tienen que pasar por B y C como intermediarios, aunque estos componentes intermedios no necesitan dichos datos.

PiniaEs la biblioteca de gestión de estado recomendada oficialmente por Vue3. Proporciona un «centro de datos» global, y cualquier componente puede leer y modificar directamente sin necesidad de transmitir capa por capa.


Instalación y registro de Pinia

Si ya elegiste Pinia al crear el proyecto, omite los pasos de instalación.

De lo contrario:

$ npm install pinia

Regístrarlo en main.js:

Ejemplo

// Ruta del archivo: src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)

// Crear la instancia de Pinia y registrarla
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')

defineStore — Definir el Store

El núcleo de Pinia esStore (almacén), cada Store gestiona un estado independiente.

Contiene tres partes:

ParteFunciónAnalogía
stateAlmacenar datosdata / ref del componente
gettersDatos derivados calculadoscomputed del componente
actionsMétodos para modificar datosmethods / function del componente

Ejemplo

// Ruta del archivo: src/stores/useFavoriteStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// defineStore('nombre del store', () => { ... })
// El segundo parámetro se escribe como una función, coherente con el estilo de la Composition API
export const useFavoriteStore = defineStore('favorite', () => {
  // ====== state: definir el estado con ref ======
  // Usar un array para almacenar los IDs de artículos favoritos
  const favoriteIds = ref([])

  // Recuperar la lista de favoritos desde localStorage
  const saved = localStorage.getItem('blog-favorites')
  if (saved) {
    favoriteIds.value = JSON.parse(saved)
  }

  // ====== getters: definir propiedades computadas con computed ======
  const favoriteCount = computed(() => favoriteIds.value.length)

  // Determinar si un artículo ya está en favoritos
  function isFavorite(articleId) {
    return favoriteIds.value.includes(articleId)
  }

  // ====== actions: definir métodos de modificación con funciones normales ======
  // Alternar el estado de favorito (favorito → cancelar, no favorito → añadir)
  function toggleFavorite(articleId) {
    const index = favoriteIds.value.indexOf(articleId)
    if (index === -1) {
      favoriteIds.value.push(articleId)     // Añadir a favoritos
    } else {
      favoriteIds.value.splice(index, 1)    // Quitar de favoritos
    }
    // Sincronizar con localStorage (guardado persistente)
    localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds.value))
  }

  return { favoriteIds, favoriteCount, isFavorite, toggleFavorite }
})

El segundo parámetro de defineStore tiene dos formas:Options Store(la forma de objeto data/methods/computed similar a Vue2) ySetup Store(la forma de función con Composition API). Se recomienda usar Setup Store, ya que es<script setup>exactamente igual a la escritura dentro de los componentes, con el menor costo de aprendizaje.


Usar el Store en los componentes

Cualquier componente puede, a través deuseFavoriteStore()obtener una instancia del store, y luego leer y escribir directamente el state y llamar a las acciones.

Añadir un botón de favoritos en BlogCard

Ejemplo

<!-- Ruta del archivo: src/components/BlogCard.vue -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const props = defineProps({
  id: Number,
  title: String,
  summary: String,
  date: String,
  category: String
})

const favoriteStore = useFavoriteStore()

// Se llama al hacer clic en el botón de favoritos
function handleFavorite(e) {
e.preventDefault() // Evita la navegación de RouterLink
  favoriteStore.toggleFavorite(props.id)
}
</script>

<template>
  <RouterLink :to="{ name: 'post', params: { id } }" class="card-link">
    <div class="card">
      <span class="tag">{{ category }}</span>
      <h3>{{ title }}</h3>
      <p>{{ summary }}</p>
      <div class="card-footer">
        <span class="date">{{ date }}</span>
        <!-- Botón de favoritos: si está en favoritos se muestra relleno, si no, vacío -->
        <button class="fav-btn" @click="handleFavorite">
          {{ favoriteStore.isFavorite(id) ? '&#x2665;' : '♡' }}
        </button>
      </div>
    </div>
  </RouterLink>
</template>

<style scoped>
.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}
.fav-btn {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #e74c3c;
  padding: 4px 8px;
}
.fav-btn:hover {
  transform: scale(1.2);
}
</style>

Añadir también un botón de favoritos en la página de detalles

Ejemplo

<!-- Ruta del archivo: src/views/PostView.vue (código parcial) -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const favoriteStore = useFavoriteStore()
// ... otro código
</script>

<template>
  <article v-if="article">
    <div class="post-header">
      <h1>{{ article.title }}</h1>
      <!-- Botón de favoritos en la página de detalles: el estado está totalmente sincronizado con la página de inicio -->
      <button class="fav-btn" @click="favoriteStore.toggleFavorite(article.id)">
        {{ favoriteStore.isFavorite(article.id) ? '&#x2665;'En favoritos' : '♡ Favorito' }}
      </button>
    </div>
    <!-- ... -->
  </article>
</template>

El estado de favoritos de la página de inicio y de la página de detalles está totalmente sincronizado: después de marcar un artículo como favorito en la página de inicio, al entrar en la página de detalles se muestra automáticamente «En favoritos».

Este es el valor de la gestión de estado global: los datos se guardan una sola vez y todos los componentes leen y escriben en la misma fuente. No se necesita emit para transmitir, ni props reenviados capa por capa.


Mostrar el número de favoritos en NavBar

Muestra el número de favoritos en el lado derecho de la barra de navegación; al hacer clic se puede ver la lista de artículos guardados.

Ejemplo

<!-- Ruta del archivo: src/components/NavBar.vue -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const favoriteStore = useFavoriteStore()
</script>

<template>
  <header class="navbar">
    <a href="/" class="logo">EJEMPLO Blog</a>
    <nav>
      <!-- Insignia con el número de favoritos -->
      <span class="fav-badge" v-if="favoriteStore.favoriteCount > 0">
Favoritos {{ favoriteStore.favoriteCount }}
      </span>
    </nav>
  </header>
</template>

<style scoped>
.fav-badge {
  background: #e74c3c;
  color: #fff;
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 13px;
}
</style>

El número en NavBar se mantiene sincronizado automáticamente con la lista de favoritos más reciente: al marcar o desmarcar un artículo, el número en la barra de navegación cambia al instante.


Resumen de las tres capacidades principales de Pinia

CapacidadCódigo correspondienteRepresentación en la función de favoritos
Compartición globalCualquier componente puede usar useXxxStore()NavBar, BlogCard y PostView comparten la misma lista de favoritos
ReactividadCuando el state cambia, todos los consumidores se actualizan automáticamenteDespués de marcar como favorito, el botón de la página de inicio se vuelve rojo, el número en la barra de navegación aumenta en 1 y la página de detalles se actualiza sincronizadamente
PersistenciaEscribir en localStorage dentro de las actionsTras actualizar la página, la lista de favoritos sigue existiendo

Resumen del capítulo

En este capítulo has dominado el uso completo de la gestión de estado global de Pinia: defineStore para definir el store (state/getters/actions), useXxxStore en los componentes para obtener la instancia, y la persistencia de datos mediante localStorage.

La función de «guardar artículos como favoritos» hace que la sincronización de datos entre la página de inicio, la página de detalles y la barra de navegación sea muy natural; todos los componentes se mantienen automáticamente consistentes.

Otras extensiones