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 astatecomputed()equivale agetters- 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ó$patchcon un objeto.'patch function': se usó$patchcon 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
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