Gestión de estado en Vue 3 para paneles admin: Pinia vs estado local
Gestión de estado en Vue 3 para paneles admin: elige entre Pinia, provide/inject y estado local usando ejemplos reales como filtros, borradores y pestañas.

Qué complica el estado en paneles admin
Los paneles de administración parecen pesados en estado porque concentran muchas piezas móviles en una sola pantalla. Una tabla no es solo datos: también tiene ordenación, filtros, paginación, filas seleccionadas y el contexto de “¿qué pasó hace un momento?” en el que confía el usuario. Añade formularios largos, permisos por rol y acciones que cambian lo que la UI debe permitir, y las decisiones pequeñas sobre el estado empiezan a importar.
El reto no es almacenar valores, sino mantener el comportamiento predecible cuando varios componentes necesitan la misma verdad. Si un filtro muestra “Activo”, la tabla, la URL y la acción de exportar deberían estar de acuerdo. Si un usuario edita un registro y navega fuera, la app no debería perder su trabajo en silencio. Si abren dos pestañas, una no debería sobrescribir a la otra.
En Vue 3, normalmente acabas eligiendo entre tres lugares para guardar estado:
- Estado local del componente: pertenece a un componente y es seguro reiniciarlo cuando se desmonta.
- provide/inject: estado compartido con ámbito de página o área de característica, sin pasar props por muchos niveles.
- Pinia: estado compartido que debe sobrevivir a la navegación, reutilizarse entre rutas y ser fácil de depurar.
Una forma útil de pensarlo: para cada pieza de estado, decide dónde debe vivir para que siga siendo correcta, no sorprenda al usuario y no se convierta en espagueti.
Los ejemplos siguientes se centran en tres problemas comunes: filtros y tablas (qué debe persistir vs reiniciarse), borradores y ediciones no guardadas (formularios en los que la gente puede confiar) y edición en múltiples pestañas (evitar colisiones de estado).
Una forma sencilla de clasificar el estado antes de elegir una herramienta
Las discusiones sobre estado se hacen más fáciles si dejas de pelear por las herramientas y primero nombras el tipo de estado que tienes. Distintos tipos de estado se comportan de forma distinta, y mezclarlos es lo que crea bugs raros.
Una separación práctica:
- Estado de UI: toggles, diálogos abiertos, filas seleccionadas, pestañas activas, orden de columna.
- Estado del servidor: respuestas API, flags de carga, errores, última vez de refresco.
- Estado de formulario: valores de campos, errores de validación, flags dirty, borradores no guardados.
- Estado cross-screen: cualquier cosa que varias rutas necesiten leer o cambiar (workspace actual, permisos compartidos).
Luego define el alcance. Pregunta dónde se usa el estado hoy, no dónde podría usarse algún día. Si solo importa dentro de un componente de tabla, el estado local suele bastar. Si dos componentes hermanos en la misma página lo necesitan, el problema real es compartir a nivel de página. Si varias rutas lo necesitan, ya estás en territorio de estado compartido de la app.
A continuación viene la duración. Algunos estados deben reiniciarse al cerrar un drawer. Otros deberían sobrevivir a la navegación (filtros cuando entras a un registro y vuelves). Algunos deben sobrevivir a un refresh (un borrador largo al que el usuario vuelve). Tratar los tres igual es lo que provoca filtros que se reinician misteriosamente o borradores que desaparecen.
Finalmente, comprueba la concurrencia. En paneles admin aparecen rápidamente casos límite: un usuario abre el mismo registro en dos pestañas, una actualización en segundo plano modifica una fila mientras un formulario está sucio, o dos editores compiten por salvar.
Ejemplo: una pantalla de “Usuarios” con filtros, una tabla y un drawer de edición. Los filtros son estado de UI con vida de página. Las filas son estado del servidor. Los campos del drawer son estado de formulario. Si el mismo usuario se edita en dos pestañas, necesitas una decisión explícita de concurrencia: bloquear, fusionar o advertir.
Una vez que puedes etiquetar el estado por tipo, alcance, duración y concurrencia, la elección de herramienta (estado local, provide/inject o Pinia) suele quedar bastante clara.
Cómo elegir: un proceso de decisión que aguanta
Las buenas decisiones de estado empiezan con un hábito: describe el estado con palabras llanas antes de escoger una herramienta. Los paneles admin mezclan tablas, filtros, formularios grandes y navegación entre registros, así que incluso un estado “pequeño” puede convertirse en imán de bugs.
Proceso de decisión en 5 pasos
-
¿Quién necesita el estado?
- Un componente: mantenlo local.
- Varios componentes bajo una misma página: considera
provide/inject. - Múltiples rutas: considera Pinia.
Los filtros son un buen ejemplo. Si solo afectan a una tabla que los contiene, el estado local está bien. Si los filtros están en un header que controla una tabla debajo, compartirlos a nivel de página (a menudo
provide/inject) mantiene todo limpio. -
¿Cuánto tiempo debe vivir?
- Si puede desaparecer al desmontarse el componente, el estado local es ideal.
- Si debe sobrevivir a un cambio de ruta, Pinia suele ser mejor.
- Si debe sobrevivir a un reload, también necesitas persistencia (storage), independientemente de dónde viva.
Esto importa especialmente para borradores. Las ediciones no guardadas son sensibles a la confianza: la gente espera que un borrador siga ahí si se alejan y vuelven.
-
¿Debe compartirse entre pestañas o aislarse por pestaña?
La edición multi-pestaña es donde se esconden bugs. Si cada pestaña debe tener su propio borrador, evita un singleton global. Prefiere estado indexado por ID del registro, o mantenlo con ámbito de página para que una pestaña no sobrescriba a otra.
-
Elige la opción más simple que encaje.
Empieza local. Sube de nivel solo cuando sientas dolor real: prop drilling, lógica duplicada o reinicios difíciles de reproducir.
-
Confirma tus necesidades de depuración.
Si necesitas una vista clara e inspeccionable de los cambios entre pantallas, las acciones y la inspección centralizada de Pinia pueden ahorrarte horas. Si el estado es de corta duración y obvio, el estado local es más fácil de leer.
Estado local del componente: cuándo es suficiente
El estado local es la opción por defecto cuando los datos solo importan a un componente en una página. Es fácil saltarse esta opción y sobreconstruir un store que pasarás meses manteniendo.
Un encaje claro es una única tabla con sus propios filtros. Si los filtros solo afectan a esa tabla (por ejemplo, la lista de Usuarios) y nada más los necesita, mantenlos como ref dentro del componente de la tabla. Lo mismo vale para pequeño estado de UI como “modal abierto?”, “qué fila se está editando?” y “qué elementos están seleccionados ahora?”.
Evita almacenar lo que puedes calcular. El badge “Filtros activos (3)” debe derivarse de los valores actuales de filtro. Etiquetas de orden, resúmenes formateados y flags de “se puede guardar” son mejores como computed porque se mantienen sincronizados automáticamente.
Las reglas de reinicio importan más que la herramienta. Decide qué se borra al cambiar de ruta (por lo general, todo), y qué se mantiene cuando el usuario cambia vistas dentro de la misma página (puede que mantengas filtros pero borres selecciones temporales para evitar acciones masivas sorpresa).
El estado local suele ser suficiente cuando:
- El estado afecta a un solo widget (un formulario, una tabla, un modal).
- Ninguna otra pantalla necesita leerlo o cambiarlo.
- Puedes mantenerlo dentro de 1–2 componentes sin pasar props por muchas capas.
- Puedes describir su comportamiento de reinicio en una frase.
El límite principal es la profundidad. Cuando empiezas a pasar el mismo estado por varios componentes anidados, el estado local se convierte en prop drilling, y esa suele ser la señal para moverte a provide/inject o a un store.
provide/inject: compartir estado dentro de una página o área de función
provide/inject está entre el estado local y un store completo. Un padre “provee” valores a todo lo que hay bajo él, y los componentes anidados los “inyectan” sin prop drilling. En paneles admin, encaja bien cuando el estado pertenece a una pantalla o área de función, no a toda la app.
Un patrón común es un shell de página que posee el estado mientras los componentes pequeños lo consumen: barra de filtros, tabla, toolbar de acciones masivas, panel de detalles y un banner de “cambios no guardados”. El shell puede proveer una superficie reactiva pequeña como un objeto filters, un objeto draftStatus (dirty, saving, error) y un par de flags de solo-lectura (por ejemplo isReadOnly según permisos).
Qué proveer (mantenlo pequeño)
Si provees todo, prácticamente recreas un store con menos estructura. Provee solo lo que varios hijos realmente necesitan. Los filtros son un ejemplo clásico: cuando la tabla, los chips, la acción de exportar y la paginación deben mantenerse en sincronía, es mejor compartir una sola fuente de verdad que malabarear props y eventos.
Claridad y riesgos
El mayor riesgo son las dependencias ocultas: un hijo “simplemente funciona” porque algo arriba le provee datos, y más tarde es difícil saber de dónde vienen las actualizaciones. Para que siga siendo legible y testeable, da a las inyecciones nombres claros (a menudo con constantes o Symbols). También prefiere proveer acciones, no solo objetos mutables. Una pequeña API como setFilter, markDirty y resetDraft hace explícita la propiedad y los cambios permitidos.
Pinia: estado compartido y actualizaciones predecibles entre pantallas
Pinia destaca cuando el mismo estado debe mantenerse consistente entre rutas y componentes. En un panel admin, eso suele significar el usuario actual, lo que puede hacer, qué workspace/organización está seleccionado y ajustes a nivel de app. Esto duele si cada pantalla lo reimplementa.
Un store ayuda porque te da un lugar único para leer y actualizar estado compartido. En lugar de pasar props por múltiples capas, importas el store donde lo necesites. Cuando vas de una lista a una página de detalle, el resto de la UI puede seguir reaccionando al mismo org seleccionado, permisos y ajustes.
Por qué Pinia resulta más fácil de mantener
Pinia propone una estructura simple: state para valores crudos, getters para valores derivados y actions para actualizaciones. En UIs admin, esa estructura evita que los “parches rápidos” se conviertan en mutaciones dispersas.
Si canEditUsers depende del rol actual más una feature flag, pon la regla en un getter. Si cambiar de org requiere limpiar selecciones en caché y recargar navegación, pon esa secuencia en una action. Terminas con menos watchers misteriosos y menos “¿por qué cambió esto?”.
Pinia además funciona bien con Vue DevTools. Cuando aparece un bug, es mucho más fácil inspeccionar el estado del store y ver qué action se ejecutó que perseguir cambios en objetos reactivos creados al azar dentro de componentes.
Evita el store-dumping
Un store global parece ordenado al principio, luego se convierte en cajón de sastre. Buenos candidatos para Pinia son preocupaciones verdaderamente compartidas como identidad de usuario, permisos, workspace seleccionado, feature flags y datos de referencia usados por varias pantallas.
Preocupaciones solo-de-página (como la entrada temporal de un formulario) deben quedarse locales a menos que varias rutas realmente las necesiten.
Ejemplo 1: filtros y tablas sin convertir todo en un store
Imagina una página de Orders: una tabla, filtros (estado, rango de fechas, cliente), paginación y un panel lateral que previsualiza el pedido seleccionado. Esto se complica rápido porque dan ganas de poner cada filtro y ajuste de tabla en un store global.
Una forma simple de elegir es decidir qué debe recordarse y dónde:
- Memoria solo (local o provide/inject): se reinicia al salir de la página. Ideal para estado desechable.
- Query params: compartibles y sobreviven al reload. Buenos para filtros y paginación que la gente copia.
- Pinia: sobrevive a la navegación. Bueno para “volver a la lista exactamente como la dejé”.
A partir de ahí, la implementación suele seguir:
Si nadie espera que la configuración sobreviva a la navegación, mantén filters, sort, page y pageSize dentro del componente de la página Orders y que esa página dispare la petición. Si la toolbar, la tabla y el panel de previsualización necesitan el mismo modelo y el paso de props se vuelve ruidoso, mueve el modelo de la lista al shell de la página y compártelo con provide/inject. Si quieres que la lista se sienta fija entre rutas (abrir un pedido, navegar y volver a la misma vista), Pinia es la mejor opción.
Una regla práctica: empieza local, usa provide/inject cuando varios hijos necesiten el mismo modelo y recurre a Pinia solo cuando realmente necesitas persistencia entre rutas.
Ejemplo 2: borradores y ediciones no guardadas (formularios que inspiran confianza)
Imagina un agente de soporte editando un cliente: datos de contacto, facturación y notas internas. Les interrumpen, cambian de pantalla y vuelven. Si el formulario olvida su trabajo o guarda datos a medias, la confianza se pierde.
Para borradores, separa tres cosas: el registro guardado más reciente, las ediciones staged del usuario y el estado solo-UI como errores de validación.
Estado local: ediciones staged con reglas claras de dirty
Si la pantalla de edición es autocontenida, el estado local del componente suele ser lo más seguro. Mantén una copia draft del registro, trackea isDirty (o un mapa dirty por campo) y guarda errores junto a los controles del formulario.
Un flujo simple: carga el registro, clónalo en draft, edita el draft y solo envía el request de guardado cuando el usuario pulsa Guardar. Cancelar descarta el draft y recarga.
provide/inject: un borrador compartido entre secciones anidadas
Los formularios admin suelen dividirse en pestañas o paneles (Perfil, Direcciones, Permisos). Con provide/inject puedes mantener un único modelo draft y exponer una API pequeña como updateField(), resetDraft() y validateSection(). Cada sección lee y escribe el mismo draft sin pasar props por cinco capas.
Cuando Pinia ayuda con los borradores
Pinia resulta útil cuando los borradores deben sobrevivir a la navegación o mostrarse fuera de la pantalla de edición. Un patrón común es draftsById[customerId], de modo que cada registro tiene su propio borrador. Esto también ayuda cuando los usuarios pueden abrir múltiples pantallas de edición.
Los bugs de borradores suelen venir de errores previsibles: crear un borrador antes de cargar el registro, sobrescribir un borrador sucio al refetch, olvidar limpiar errores al cancelar o usar una clave compartida que hace que los borradores se pisen entre sí. Si estableces reglas claras (cuándo crear, sobrescribir, descartar, persistir y reemplazar tras guardar), la mayoría desaparecen.
Si construyes pantallas admin con AppMaster (appmaster.io), la separación “borrador vs registro guardado” sigue aplicando: mantén el borrador en el cliente y trata al backend como fuente de verdad solo tras un Save exitoso.
Ejemplo 3: edición multi-pestaña sin colisiones de estado
La edición en varias pestañas es donde los paneles admin suelen romperse. Un usuario abre Cliente A, luego Cliente B, cambia entre ellas y espera que cada pestaña recuerde sus cambios no guardados.
La solución es modelar cada pestaña como su propio paquete de estado, no como un solo borrador compartido. Cada pestaña necesita al menos una clave única (normalmente basada en el ID del registro), los datos del draft, su estado (clean, dirty, saving) y errores por campo.
Si las pestañas viven dentro de una misma pantalla, un enfoque local funciona bien. Mantén la lista de pestañas y los borradores propiedad del componente de la página que renderiza las pestañas. Cada panel editor lee y escribe solo su propio paquete. Al cerrar una pestaña, elimina ese paquete y listo. Esto mantiene el aislamiento y la razonabilidad.
Sea donde sea que viva el estado, la forma es parecida:
- Una lista de objetos de pestañas (cada uno con su
customerId,draft,statusyerrors). - Una
activeTabKey. - Acciones como
openTab(id),updateDraft(key, patch),saveTab(key)ycloseTab(key).
Pinia tiene sentido cuando las pestañas deben sobrevivir a la navegación (ir a Orders y volver) o cuando varias pantallas necesitan abrir y enfocar pestañas. En ese caso, un pequeño store “tab manager” mantiene el comportamiento consistente en la app.
La colisión principal a evitar es una variable global única como currentDraft. Funciona hasta que se abre la segunda pestaña: entonces las ediciones se sobreescriben, los errores de validación aparecen en el lugar equivocado y Guardar actualiza el registro incorrecto. Cuando cada pestaña tiene su propio paquete, las colisiones desaparecen por diseño.
Errores comunes que causan bugs y código desordenado
La mayoría de los bugs en paneles admin no son “bugs de Vue”. Son bugs de estado: los datos viven en el lugar equivocado, dos partes de la pantalla discrepan o el estado viejo permanece en silencio.
Aquí los patrones más frecuentes:
Poner todo en Pinia por defecto vuelve la propiedad confusa. Un store global parece ordenado al principio, pero pronto cada página lee y escribe los mismos objetos y la limpieza se vuelve conjetural.
Usar provide/inject sin un contrato claro crea dependencias ocultas. Si un hijo inyecta filters pero no hay un entendimiento compartido de quién lo provee y qué acciones pueden cambiarlo, obtendrás actualizaciones sorpresa cuando otro hijo empiece a mutar el mismo objeto.
Mezclar estado del servidor y UI en el mismo store causa sobreescrituras accidentales. Los registros fetched se comportan distinto que “¿está el drawer abierto?”, “pestaña actual” o “campos dirty”. Si coexisten, el refetch puede pisar la UI o los cambios de UI pueden mutar datos cacheados.
Omitir la limpieza del ciclo de vida deja que el estado se filtre. Filtros de una vista pueden afectar a otra y borradores pueden permanecer tras salir de la página. La próxima vez que alguien abra un registro diferente verá selecciones antiguas y asumirá que la app está rota.
Claves pobres para borradores son un silencioso destructor de confianza. Si guardas borradores bajo una sola clave como draft:editUser, editar Usuario A y luego Usuario B sobreescribe el mismo borrador.
Una regla simple previene la mayoría: mantén el estado lo más cerca posible de donde se usa, y elévalo solo cuando dos partes verdaderamente independientes necesiten compartirlo. Cuando lo compartas, define la propiedad (quién puede cambiarlo) y la identidad (cómo se indexa).
Lista rápida antes de elegir local, provide/inject o Pinia
La pregunta más útil es: ¿quién posee este estado? Si no lo puedes decir en una frase, probablemente el estado hace demasiado y debería dividirse.
Usa estas comprobaciones como filtro rápido:
- ¿Puedes nombrar al dueño (un componente, una página o toda la app)?
- ¿Debe sobrevivir a cambios de ruta o a un reload? Si sí, planifica persistencia en lugar de depender del navegador.
- ¿Se editarán dos registros a la vez? Si sí, clavea el estado por ID.
- ¿El estado solo es usado por componentes bajo un mismo shell de página? Si sí,
provide/injectencaja. - ¿Necesitas inspeccionar cambios y saber quién cambió qué? Si sí, Pinia suele ser el lugar más claro para esa porción.
Resumiendo en términos llanos:
Si el estado nace y muere dentro de un componente (por ejemplo, un dropdown abierto/cerrado), mantenlo local. Si varios componentes en la misma pantalla necesitan contexto compartido (barra de filtros + tabla + resumen), provide/inject lo comparte sin hacerlo global. Si el estado debe compartirse entre pantallas, sobrevivir a la navegación o requiere actualizaciones predecibles y depurables, usa Pinia y clavea entradas por ID cuando haya borradores.
Si estás construyendo una UI admin con Vue 3 (incluidas las generadas con herramientas como AppMaster), este checklist te ayuda a evitar poner todo en un store demasiado pronto.
Próximos pasos: evolucionar el estado sin crear un desastre
La forma más segura de mejorar la gestión de estado en paneles admin es hacerlo en pasos pequeños y aburridos. Empieza con estado local para todo lo que se quede dentro de una página. Cuando veas reutilización real (lógica repetida, un tercer componente que necesita el mismo estado), elévalo un nivel. Solo entonces considera un store compartido.
Un camino que funciona para la mayoría de equipos:
- Mantén el estado solo-de-página local primero (filtros, orden, paginación, paneles abiertos/cerrados).
- Usa
provide/injectcuando varios componentes de la misma página necesiten contexto compartido. - Añade un store Pinia a la vez para necesidades entre pantallas (draft manager, tab manager, workspace actual).
- Escribe reglas de reinicio y síguelas (qué se reinicia en navegación, logout, Clear filters, Discard changes).
Las reglas de reinicio parecen pequeñas, pero evitan la mayoría de los “¿por qué cambió esto?”. Decide, por ejemplo, qué sucede con un borrador cuando alguien abre otro registro y vuelve: restaurar, advertir o resetear. Luego haz ese comportamiento consistente.
Si introduces un store, mantenlo por características. Un store de borradores debe manejar crear, restaurar y limpiar borradores, pero no debería también poseer filtros de tabla o flags de layout UI.
Si quieres prototipar un panel admin rápidamente, AppMaster (appmaster.io) puede generar una app web Vue3 más backend y lógica, y aún puedes refinar el código generado donde necesites manejo de estado personalizado. Un paso práctico es construir una pantalla completa (por ejemplo, un formulario de edición con recuperación de borradores) y ver qué necesita realmente Pinia frente a qué puede quedarse local.
FAQ
Usa estado local cuando los datos solo afectan a un componente y pueden reiniciarse al desmontarlo. Ejemplos típicos: diálogo abierto/cerrado, filas seleccionadas en una tabla y una sección de formulario que no se reutiliza en otro sitio.
Emplea provide/inject cuando varios componentes de la misma página necesiten una fuente de verdad compartida y el paso de props se vuelva ruidoso. Mantén lo que provees pequeño e intencional para que la página sea fácil de razonar.
El indicador más claro es que el estado debe compartirse entre rutas, sobrevivir a la navegación o ser fácil de inspeccionar y depurar en un solo lugar. Ejemplos comunes: workspace actual, permisos, feature flags y gestores compartidos como borradores o pestañas.
Empieza por nombrar el tipo (UI, servidor, formulario, cross-screen), luego decide el alcance (un componente, una página, varias rutas), la vida útil (se reinicia al desmontar, sobrevive a la navegación, sobrevive al reload) y la concurrencia (editor único o multi-pestaña). De esas cuatro etiquetas suele salir la herramienta adecuada.
Si los usuarios esperan compartir o restaurar la vista, usa query params para filtros y paginación. Si esperan “volver a la lista tal como la dejaron” entre rutas, guarda el modelo en Pinia. Si no, mantenlo con alcance de página.
Separa el registro guardado del último estado, del borrador del usuario y del estado solo-UI como errores de validación. Solo escribe en el backend al hacer Save. Lleva una regla clara de dirty y decide qué hacer en navegación (advertir, auto-save o mantener borrador recuperable).
Asigna a cada editor abierto su propio paquete de estado con clave basada en el ID del registro (y a veces una clave de pestaña). Evita una única variable global como currentDraft que hace que las pestañas se sobreescriban entre sí.
Un provide/inject controlado por la página funciona si el flujo de edición está confinado a una ruta. Si los borradores deben sobrevivir a cambios de ruta o ser accesibles fuera de la pantalla de edición, Pinia con draftsById[recordId] suele ser más simple y predecible.
No almacenes lo que puedes computar. Deriva badges, resúmenes y flags de “puede guardar” desde el estado actual con computed para que no se desincronicen.
Poner todo en Pinia por defecto, mezclar respuestas del servidor con toggles UI, y no limpiar el estado en la navegación son las fuentes más comunes de comportamientos raros. También evita claves pobres, por ejemplo un solo draft que se reusa para distintos registros.


