Ir al contenido
LUIGI MICCA

Publicado

4 min de lectura

Estado persistente en Vue 3, sin recurrir a Pinia

Tema, filtros, borradores, carritos: la mayoría de los problemas de 'state management' son en realidad problemas de persistencia. Cómo hacer que el estado de Vue sobreviva a recargas y pestañas — a mano, y en una línea.

Toda app Vue acaba acumulando un puñado de valores que deben sobrevivir a una recarga de página: el tema elegido, un filtro de búsqueda, un formulario a medias, un carrito. Y todo equipo acaba teniendo la misma conversación: "¿añadimos Pinia para esto?"

Normalmente, no. Un store resuelve el estado compartido; estos son valores persistentes — y pegarle persistencia a un store te deja igualmente escribiendo la misma fontanería. Miremos de cerca esa fontanería, porque los bugs viven en los detalles.

La versión artesanal (y sus trampas)

El enfoque ingenuo cabe en seis líneas:

import { ref, watch } from 'vue';

const theme = ref(JSON.parse(localStorage.getItem('theme') ?? '"light"'));
watch(theme, (value) => localStorage.setItem('theme', JSON.stringify(value)));

Funciona — en tu máquina, hoy. En producción tiene al menos cinco agujeros:

  1. En SSR revienta. En Nuxt o en cualquier render de servidor, localStorage no existe. Primer error de hydration del sprint.
  2. Los datos corruptos lanzan excepciones. Basta un valor malformado en el storage (una release antigua, una extensión, un usuario curioso) y JSON.parse se lleva el componente por delante.
  3. Las pestañas no se hablan. Cambias el tema en una pestaña y las otras cinco conservan el antiguo hasta recargar. El evento storage lo arregla — si te acuerdas de usarlo, y de quitar el listener; si no, acabas de fabricar una fuga de memoria.
  4. Los valores caducos viven para siempre. Un token cacheado, un borrador del mes pasado: sin caducidad, el storage se convierte en un cajón de sastre del que tu app se fía a ciegas.
  5. Tormentas de escrituras. Persistir un campo de texto en cada pulsación machaca una API síncrona en el hilo principal.

Ninguno de estos problemas es difícil. Todos juntos, repetidos en cada componente que los necesita, son exactamente el tipo de código que en una codebase acaba multiplicándose en cinco implementaciones ligeramente distintas — me gano la vida auditando front-ends, y te aseguro que pasa.

La versión en una línea

De ahí nace vue-smart-state, un pequeño composable open source que mantengo yo (~2 kB, cero dependencias — transparencia total: el paquete es mío):

import { useState } from 'vue-smart-state';

const [theme, setTheme] = useState<'light' | 'dark'>('light', {
  persist: true,
  storageKey: 'theme',
  syncTabs: true,
});

Una llamada cubre toda la lista: restauración al cargar, persistencia en cada cambio, sincronización entre pestañas con limpieza correcta de listeners, seguridad en SSR y una gestión de errores que se degrada con elegancia en lugar de lanzar excepciones. La API calca deliberadamente el useState de React — tupla más updater funcional (setTheme(t => …)) — porque la mitad de los desarrolladores que llegan a un equipo Vue traen esa memoria muscular consigo.

Los detalles de producción están a una opción de distancia:

// Una sesión que caduca sola
const [token, setToken] = useState('', {
  persist: true,
  storageKey: 'token',
  ttl: 15 * 60 * 1000,
});

// Un borrador que no machaca el storage en cada pulsación
const [draft, setDraft] = useState('', {
  persist: true,
  storageKey: 'draft',
  writeDebounce: 300,
});

Y para Nuxt hay un módulo de una línea, nuxt-smart-state, que auto-importa el composable como useSmartState — con alias a propósito, porque el useState de Nuxt resuelve otro problema (estado de servidor por petición, no persistencia en el navegador).

Cuándo sí quieres un store

Rincón de la honestidad: si muchos componentes lejanos mutan el mismo estado complejo, si necesitas el time-travel de las devtools, o si a tu equipo le conviene un único patrón consagrado para los datos globales — usa Pinia, es excelente. La cuestión no es "los stores son malos"; es que la persistencia no es un motivo para adoptar uno. Elige el store para compartir, un composable de persistencia para persistir, y deja que cada uno haga su trabajo.

La lección más amplia es de las que repito en el trabajo de arquitectura: la mayoría de las conversaciones de "necesitamos una solución de state management" se disuelven en cuanto le pones nombre al requisito real. Compartir, persistir, sincronizar y cachear son cuatro problemas distintos con la misma camiseta puesta.


Soy arquitecto de software independiente con foco en front-end — y de vez en cuando empaqueto bien las partes aburridas, como el composable de arriba. Si tu codebase Vue tiene cinco maneras de hacer lo mismo, hablemos: una llamada de 30 minutos es gratuita.