Pinia: Setup Stores y composición

Intermedio
Vuejs
Vuejs
Actualizado: 01/04/2026

Setup Stores

Los Setup Stores son la segunda forma de definir stores en Pinia. En lugar de pasar un objeto de opciones, se pasa una función que utiliza la Composition API de Vue. Dentro de esta función:

  • ref() equivale a state
  • computed() equivale a getters
  • funciones normales equivalen a actions
// src/stores/counterStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // State
  const count = ref(0)
  const step = ref(1)

  // Getters
  const doubleCount = computed(() => count.value * 2)
  const isPositive = computed(() => count.value > 0)

  // Actions
  function increment() {
    count.value += step.value
  }

  function decrement() {
    count.value -= step.value
  }

  function setStep(newStep: number) {
    step.value = newStep
  }

  async function fetchInitialValue() {
    const response = await fetch('/api/counter')
    const data = await response.json()
    count.value = data.value
  }

  return {
    count,
    step,
    doubleCount,
    isPositive,
    increment,
    decrement,
    setStep,
    fetchInitialValue
  }
})

Todo lo que se retorna en la función queda expuesto como propiedades del store. Lo que no se retorna permanece privado e inaccesible desde los componentes.

Options Store vs Setup Store

Ambos patrones son válidos y oficialmente soportados. La elección depende del contexto:

| Característica | Options Store | Setup Store | |---|---|---| | Sintaxis | Objeto con state/getters/actions | Función con ref/computed/funciones | | Familiaridad | Similar a Options API | Similar a Composition API | | Flexibilidad | Estructura fija | Lógica libre, watchers, composables | | $reset() | Funciona automáticamente | Requiere implementación manual | | TypeScript | Buena inferencia | Inferencia completa | | Composables | No se pueden usar directamente | Se integran de forma natural |

El Setup Store es preferible cuando se necesita usar watchers, composables externos o lógica condicional compleja. El Options Store es más conveniente para stores simples y directos.

storeToRefs

Cuando se desestructuran propiedades de un store, las propiedades de estado y getters pierden la reactividad. La función storeToRefs soluciona este problema creando refs para cada propiedad reactiva:

flowchart TB
  U["useCounterStore()"]
  B["Desestructurar state o getters sin storeToRefs"]
  R["storeToRefs(store) para state y getters"]
  A["Desestructurar acciones desde el store"]
  B --> X["Pierdes reactividad en esas propiedades"]
  R --> Y["Reactividad preservada"]
  U --> B
  U --> R
  U --> A
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counterStore'

const counterStore = useCounterStore()

// Sin storeToRefs: se pierde la reactividad
const { count, doubleCount } = counterStore // NO reactivo

// Con storeToRefs: se mantiene la reactividad
const { count, doubleCount } = storeToRefs(counterStore) // Reactivo

// Las acciones se pueden desestructurar directamente
const { increment, decrement, setStep } = counterStore
</script>

<template>
  <div>
    <p>Contador: {{ count }}</p>
    <p>Doble: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

storeToRefs solo crea refs para las propiedades reactivas (state y getters), ignorando las acciones. Por ello, las acciones siempre se desestructuran directamente del store.

Composición de stores

Un store puede usar otro store en su interior, tanto en Options Stores como en Setup Stores. Esto permite crear relaciones entre dominios de estado:

// src/stores/userStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export interface User {
  id: number
  name: string
  role: 'admin' | 'editor' | 'viewer'
}

export const useUserStore = defineStore('user', () => {
  const currentUser = ref<User | null>(null)
  const isAuthenticated = computed(() => currentUser.value !== null)
  const isAdmin = computed(() => currentUser.value?.role === 'admin')

  function login(user: User) {
    currentUser.value = user
  }

  function logout() {
    currentUser.value = null
  }

  return { currentUser, isAuthenticated, isAdmin, login, logout }
})
// src/stores/settingsStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useUserStore } from './userStore'

export const useSettingsStore = defineStore('settings', () => {
  const userStore = useUserStore()

  const theme = ref<'light' | 'dark'>('light')
  const language = ref('es')

  const canEditSettings = computed(() => userStore.isAdmin)

  function toggleTheme() {
    if (!userStore.isAuthenticated) {
      throw new Error('Debe iniciar sesión para cambiar el tema')
    }
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }

  function setLanguage(lang: string) {
    if (!canEditSettings.value) {
      throw new Error('Solo administradores pueden cambiar el idioma')
    }
    language.value = lang
  }

  return { theme, language, canEditSettings, toggleTheme, setLanguage }
})

El store useSettingsStore consume useUserStore internamente para condicionar sus operaciones según el estado de autenticación.

$subscribe: observar mutaciones de estado

El método $subscribe permite reaccionar a los cambios del estado de un store. Funciona de manera similar a watch pero está optimizado para stores:

<script setup lang="ts">
import { useCounterStore } from '@/stores/counterStore'

const counterStore = useCounterStore()

counterStore.$subscribe((mutation, state) => {
  console.log('Tipo de mutación:', mutation.type)
  // 'direct' | 'patch object' | 'patch function'
  console.log('ID del store:', mutation.storeId)
  console.log('Estado actual:', state)
})
</script>

Los posibles valores de mutation.type son:

  • 'direct': el estado se modificó directamente (ej: store.count++).
  • 'patch object': se usó $patch con un objeto.
  • 'patch function': se usó $patch con una función.

Por defecto, la suscripción se vincula al componente y se elimina cuando este se desmonta. Para mantenerla activa tras el desmontaje:

counterStore.$subscribe(
  (mutation, state) => {
    localStorage.setItem('counter', JSON.stringify(state))
  },
  { detached: true }
)

$onAction: interceptar acciones

El método $onAction permite interceptar las acciones antes y después de su ejecución:

<script setup lang="ts">
import { useCounterStore } from '@/stores/counterStore'

const counterStore = useCounterStore()

counterStore.$onAction(({ name, args, after, onError }) => {
  const startTime = Date.now()
  console.log(`Acción "${name}" iniciada con args:`, args)

  after((result) => {
    const duration = Date.now() - startTime
    console.log(`Acción "${name}" completada en ${duration}ms`)
    console.log('Resultado:', result)
  })

  onError((error) => {
    console.error(`Acción "${name}" falló:`, error)
  })
})
</script>

El callback de $onAction recibe un objeto con:

  • name: nombre de la acción ejecutada.
  • args: argumentos pasados a la acción.
  • after(callback): ejecuta un callback tras completarse la acción (incluyendo promesas resueltas).
  • onError(callback): ejecuta un callback si la acción lanza un error.
  • store: referencia al store.

$reset: reiniciar el estado

El método $reset restaura el estado del store a sus valores iniciales. Funciona automáticamente en Options Stores:

const counterStore = useCounterStore()
counterStore.count = 42
counterStore.$reset() // count vuelve a 0

En Setup Stores, $reset no está disponible automáticamente porque Pinia no puede determinar cuál es el estado inicial. Se debe implementar manualmente:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const step = ref(1)

  function $reset() {
    count.value = 0
    step.value = 1
  }

  return { count, step, $reset }
})

$patch: actualizaciones en lote

El método $patch permite modificar varias propiedades del estado simultáneamente en una única mutación. Acepta dos sintaxis:

Sintaxis de objeto: se pasa un objeto parcial con las propiedades a modificar.

const counterStore = useCounterStore()

counterStore.$patch({
  count: 10,
  step: 5
})

Sintaxis de función: se recibe el estado completo y se modifica directamente. Es útil para operaciones complejas como manipular arrays:

import { useTaskStore } from '@/stores/taskStore'

const taskStore = useTaskStore()

taskStore.$patch((state) => {
  state.tasks.push({ id: Date.now(), title: 'Nueva tarea', completed: false })
  state.isLoading = false
})

La principal ventaja de $patch es que agrupa todas las modificaciones en una sola entrada del historial de DevTools y dispara una única notificación a $subscribe, en lugar de una por cada cambio individual.

Ejemplo completo

El siguiente ejemplo muestra un store de notificaciones que demuestra varias de las funcionalidades vistas:

// src/stores/notificationStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface Notification {
  id: number
  message: string
  type: 'info' | 'success' | 'warning' | 'error'
  read: boolean
}

export const useNotificationStore = defineStore('notification', () => {
  const notifications = ref<Notification[]>([])

  const unreadCount = computed(
    () => notifications.value.filter(n => !n.read).length
  )

  const hasUnread = computed(() => unreadCount.value > 0)

  function addNotification(message: string, type: Notification['type'] = 'info') {
    notifications.value.push({
      id: Date.now(),
      message,
      type,
      read: false
    })
  }

  function markAsRead(id: number) {
    const notification = notifications.value.find(n => n.id === id)
    if (notification) {
      notification.read = true
    }
  }

  function markAllAsRead() {
    notifications.value.forEach(n => { n.read = true })
  }

  function clearAll() {
    notifications.value = []
  }

  function $reset() {
    notifications.value = []
  }

  return {
    notifications,
    unreadCount,
    hasUnread,
    addNotification,
    markAsRead,
    markAllAsRead,
    clearAll,
    $reset
  }
})
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useNotificationStore } from '@/stores/notificationStore'

const notificationStore = useNotificationStore()
const { notifications, unreadCount, hasUnread } = storeToRefs(notificationStore)
const { addNotification, markAsRead, markAllAsRead, clearAll } = notificationStore

// Registrar suscripción para auditoría
notificationStore.$subscribe((mutation, state) => {
  console.log(`[${mutation.storeId}] Cambio tipo: ${mutation.type}`)
})

// Interceptar acciones para logging
notificationStore.$onAction(({ name, after }) => {
  after(() => {
    console.log(`Acción completada: ${name}`)
  })
})
</script>

<template>
  <div>
    <h2>Notificaciones ({{ unreadCount }} sin leer)</h2>

    <div>
      <button @click="addNotification('Operación exitosa', 'success')">
        Añadir éxito
      </button>
      <button @click="addNotification('Algo salió mal', 'error')">
        Añadir error
      </button>
      <button v-if="hasUnread" @click="markAllAsRead">
        Marcar todas como leídas
      </button>
      <button @click="clearAll">Limpiar</button>
    </div>

    <ul>
      <li
        v-for="notification in notifications"
        :key="notification.id"
        :class="{ 'font-bold': !notification.read }"
      >
        [{{ notification.type }}] {{ notification.message }}
        <button v-if="!notification.read" @click="markAsRead(notification.id)">
          Marcar leída
        </button>
      </li>
    </ul>
  </div>
</template>
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