Gestión de estado en React: mantenla sencilla en aplicaciones generadas
Gestión de estado en React simplificada: separa el estado del servidor del estado del cliente, sigue unas reglas y detecta pronto los signos de complejidad creciente.

Qué sale mal con el estado en apps React reales
El estado es cualquier dato que puede cambiar mientras tu app se ejecuta. Eso incluye lo que ves (un modal abierto), lo que estás editando (un borrador de formulario) y los datos que obtienes (una lista de proyectos). El problema es que todo eso se llama "estado", aunque se comporta de formas muy distintas.
La mayoría de las apps desordenadas fallan de la misma manera: demasiados tipos de estado se mezclan en el mismo sitio. Un componente acaba teniendo datos del servidor, flags de UI, borradores de formulario y valores derivados, y trata de mantenerlos alineados con efectos. Pronto ya no puedes responder preguntas sencillas como "¿de dónde viene este valor?" o "¿qué lo actualiza?" sin buscar en varios archivos.
Las apps React generadas se desvían hacia esto más rápido porque es fácil aceptar la primera versión que funciona. Añades una nueva pantalla, copias un patrón, parcheas un bug con otro useEffect, y ahora tienes dos fuentes de verdad. Si el generador o el equipo cambian de dirección a mitad de camino (estado local aquí, store global allá), la base de código acumula patrones en lugar de construirse sobre uno solo.
El objetivo es aburrido: menos tipos de estado y menos lugares donde mirar. Cuando existe un hogar obvio para los datos del servidor y uno obvio para el estado solo de UI, los bugs se hacen más pequeños y los cambios dejan de parecer riesgosos.
"Mantenlo sencillo" significa seguir unas pocas reglas:
- No reflejes datos del servidor en estado cliente salvo que tengas una razón clara.
- No promociones estado local de UI a una store global solo porque pueda ser útil en el futuro.
- No dejes que valores derivados se conviertan en su propio estado a menos que calcularlos sea realmente caro.
Un ejemplo concreto: si una lista de usuarios viene del backend, trátala como estado del servidor y obténla donde se usa. Si selectedUserId existe solo para controlar un panel de detalles, mantenlo como estado UI cerca de ese panel. Mezclar esos dos es donde empieza la complejidad.
Estado del servidor vs estado del cliente en lenguaje llano
La mayoría de los problemas de estado en React comienzan con una confusión: tratar datos del servidor como estado de UI. Sepáralos desde el principio y la gestión de estado se mantiene tranquila, incluso cuando tu app crece.
El estado del servidor pertenece al backend: usuarios, órdenes, tareas, permisos, precios, feature flags. Puede cambiar sin que tu app haga nada (otra pestaña lo actualiza, un admin lo edita, un job corre, los datos expiran). Como es compartido y cambiante, necesitas fetching, caching, refetching y manejo de errores.
El estado del cliente es lo que solo le importa a tu UI ahora mismo: qué modal está abierto, qué pestaña está seleccionada, un toggle de filtro, orden de sort, una barra lateral colapsada, un borrador de búsqueda. Si cierras la pestaña, está bien perderlo.
Una prueba rápida es: "¿Podría refrescar la página y reconstruir esto desde el servidor?"
- Si sí, probablemente es estado del servidor.
- Si no, probablemente es estado del cliente.
También existe el estado derivado, que te evita crear estado adicional. Es un valor que puedes calcular a partir de otros valores, así que no lo almacenas. Listas filtradas, totales, isFormValid y "mostrar estado vacío" suelen pertenecer aquí.
Ejemplo: obtienes una lista de proyectos (estado del servidor). El filtro seleccionado y el flag de diálogo "Nuevo proyecto" son estado del cliente. La lista visible tras filtrar es estado derivado. Si guardas la lista visible por separado, se desincronizará y perseguirás bugs tipo "¿por qué está obsoleta?".
Esta separación ayuda cuando una herramienta como Koder.ai genera pantallas rápidamente: mantén los datos del backend en una capa de fetching, mantiene las elecciones de UI cerca de los componentes y evita almacenar valores computados.
Un conjunto aburrido de reglas que previene la mayoría de problemas
El estado se vuelve doloroso cuando un dato tiene dos dueños. La forma más rápida de mantenerlo simple es decidir quién posee qué y apegarse a ello.
- Trata los datos de la API como algo que obtienes y cacheas, no como algo que mantienes manualmente en el estado del componente.
- No clones datos obtenidos en estado local "por si acaso". Las copias crean deriva.
- Mantén el estado cliente cerca de donde se usa. Sube su alcance solo cuando partes separadas del árbol realmente lo necesiten.
- Almacena IDs y flags pequeños, no objetos enteros. Re-deriva el objeto desde la cache cuando renderices.
Ejemplo: obtienes una lista de usuarios y muestras detalles cuando uno está seleccionado. Un error común es almacenar el objeto completo del usuario seleccionado en el estado. Guarda selectedUserId en su lugar. Mantén la lista en la cache del servidor. La vista de detalles busca el usuario por ID, así que los refetches actualizan la UI sin código extra de sincronización.
En apps React generadas, también es fácil aceptar estado "útil" generado que duplica datos del servidor. Cuando veas código que hace fetch -> setState -> edit -> refetch, pausa. Eso suele ser una señal de que estás construyendo una segunda base de datos en el navegador.
Cómo manejar el estado del servidor sin sobrepensarlo
El estado del servidor es cualquier cosa que vive en el backend: listas, páginas de detalle, resultados de búsqueda, permisos, contadores. El enfoque aburrido es elegir una herramienta para ello y apegarse a ella. Para muchas apps React, TanStack Query es suficiente.
El objetivo es sencillo: los componentes piden datos, muestran loading y errores, y no se preocupan por cuántas llamadas fetch suceden bajo el capó. Esto importa en apps generadas porque pequeñas inconsistencias se multiplican rápido al añadir nuevas pantallas.
Trata las claves de consulta como un sistema de nombres, no como una ocurrencia posterior. Mantenlas consistentes: claves en arrays estables, solo incluye entradas que cambian el resultado (filtros, página, orden), y prefiere unas pocas formas predecibles en lugar de muchos casos únicos. Muchos equipos también ponen la construcción de claves en pequeños helpers para que cada pantalla use las mismas reglas.
Para escrituras, usa mutations con manejo explícito en el success. Una mutation debe responder dos preguntas: ¿qué cambió? y ¿qué debe hacer la UI después?
Ejemplo: creas una tarea nueva. En el success, o invalidas la query de lista de tareas (para que recargue una vez) o haces una actualización dirigida en la cache (añadir la tarea nueva a la lista cacheada). Elige una aproximación por característica y mantenla consistente.
Si te sientes tentado a añadir llamadas de refetch en varios lugares "por si acaso", elige un movimiento aburrido en su lugar:
- Invalida la clave de query exacta que quedó obsoleta.
- Actualiza la cache para la query exacta que cambió.
- Navega a una pantalla que ya hace el fetch de la query correcta.
Patrones de estado cliente que se mantienen pequeños
El estado cliente es lo que el navegador posee: un flag de sidebar abierto, una fila seleccionada, texto de filtro, un borrador antes de guardar. Mantenlo cerca de donde se usa y por lo general se mantiene manejable.
Empieza pequeño: useState en el componente más cercano. Cuando generas pantallas (por ejemplo con Koder.ai), es tentador empujar todo a una store global "por si acaso". Ahí es donde terminas con una store que nadie entiende.
Una regla simple para promover estado
Solo mueve estado hacia arriba cuando puedas nombrar el problema de compartición.
- Mantén el estado solo de UI local por defecto.
- Súbelo al padre común más cercano cuando los hermanos lo necesiten.
- Usa una store compartida mínima solo cuando múltiples rutas o componentes distantes necesiten el mismo valor al mismo tiempo.
Ejemplo: una tabla con un panel de detalles puede mantener selectedRowId en el componente tabla. Si una barra de herramientas en otra parte de la página también lo necesita, súbelo al componente de la página. Si una ruta separada (como edición masiva) lo necesita, ahí tiene sentido una pequeña store.
Formas de store que permanecen legibles
Si usas una store (Zustand o similar), mantenla enfocada en un trabajo. Almacena el "qué" (IDs seleccionados, filtros), no los "resultados" (listas ordenadas) que puedes derivar.
Cuando una store empieza a crecer, pregúntate: ¿sigue siendo una sola feature? Si la respuesta honesta es "más o menos", sepárala ahora, antes de que la siguiente característica la convierta en una bola de estado que da miedo tocar.
Formularios, borradores y estado UI temporal
Los bugs de formularios suelen venir de mezclar tres cosas: lo que el usuario escribe, lo que el servidor ha guardado y lo que la UI muestra.
Para una gestión aburrida del estado, trata el formulario como estado cliente hasta que lo envíes. Los datos del servidor son la última versión guardada. El formulario es un borrador. No edites el objeto del servidor in situ. Copia valores en el estado borrador, deja que el usuario los cambie libremente, luego envía y refetch (o actualiza la cache) en success.
Decide pronto qué debe persistir cuando el usuario navega fuera. Esa única elección evita muchos bugs sorpresa. Por ejemplo, el modo de edición inline y dropdowns abiertos deberían generalmente resetear, mientras que un borrador largo de un asistente o un mensaje no enviado podría persistir. Persiste tras reload solo cuando los usuarios lo esperan claramente (como un formulario de checkout).
Mantén las reglas de validación en un solo lugar. Si las repartes entre inputs, handlers de submit y helpers, acabarás con errores descoordinados. Prefiere un solo esquema (o una función validate()), y deja que la UI decida cuándo mostrar errores (on change, on blur o on submit).
Ejemplo: generas una pantalla Edit Profile en Koder.ai. Carga el perfil guardado como estado del servidor. Crea estado borrador para los campos del formulario. Muestra "cambios sin guardar" comparando borrador vs guardado. Si el usuario cancela, descarta el borrador y muestra la versión del servidor. Si guarda, envía el borrador y luego reemplaza la versión guardada con la respuesta del servidor.
Paso a paso: simplificar una configuración de estado desordenada
A medida que una app generada crece, es común acabar con los mismos datos en tres sitios: estado de componente, una store global y una cache. La solución normalmente no es una librería nueva. Es elegir un hogar por cada pieza de estado.
Un flujo de limpieza que funciona en la mayoría de apps:
- Haz inventario de tu estado. Lista lo que tienes (listas, elemento seleccionado, filtros, modales, borradores) y etiqueta cada cosa como servidor o cliente.
- Borra duplicados. Elimina estado como
filteredUserssi puedes calcularlo desdeusers+filter. PrefiereselectedUserIdsobre unselectedUserduplicado. - Centraliza el fetching en una capa. Usa un enfoque de server-state para que caching, refetching e invalidación tengan un libro de reglas.
- Añade una store cliente mínima solo donde realmente valga la pena (necesidades de UI entre páginas, como un borrador de asistente).
- Fija reglas de nombres para que el desorden no vuelva.
Ejemplo: una app CRUD generada con Koder.ai a menudo empieza con un useEffect fetch más una copia global de la misma lista. Tras centralizar el estado del servidor, la lista viene de una sola query y "refrescar" pasa a ser invalidación en lugar de sincronización manual.
Para nombres, mantenlo consistente y aburrido:
- Queries:
users.list,users.detail(id) - Estado cliente:
ui.isCreateModalOpen,filters.userSearch - Acciones:
openCreateModal(),setUserSearch(value) - Escrituras al servidor:
users.create,users.update,users.delete
El objetivo es una fuente de verdad por cosa, con límites claros entre estado del servidor y estado del cliente.
Signos tempranos de que la complejidad del estado está a punto de explotar
Los problemas de estado empiezan pequeños, luego un día cambias un campo y tres partes de la UI no se ponen de acuerdo sobre el valor "real".
La señal más clara es datos duplicados: el mismo usuario o carrito vive en un componente, en una store global y en una cache de requests. Cada copia se actualiza en un momento distinto, y añades más código solo para mantenerlas iguales.
Otra señal es el código de sincronización: efectos que empujan estado de un lado a otro. Patrones como "cuando cambia la query, actualiza la store" y "cuando la store cambia, refetch" pueden funcionar hasta que un caso límite provoca valores obsoletos o bucles.
Algunas banderas rápidas:
- No puedes explicar "de dónde viene este valor" y "quién lo posee" en una frase.
- Tu store o reducer importa clientes de API o conoce códigos HTTP.
- Cada nueva pantalla añade flags compartidos como
needsRefresh,didInit,isSavingque nadie borra. - Escribes efectos principalmente para reflejar un estado en otro.
- Arreglar un bug implica actualizar el mismo campo en múltiples capas.
Ejemplo: generas un dashboard en Koder.ai y añades un modal Edit Profile. Si los datos del perfil están en la cache de queries, copiados en una store global y duplicados en estado local de formulario, ahora tienes tres fuentes de verdad. La primera vez que añades refetching en segundo plano u optimistic updates, aparecen desajustes.
Cuando veas estas señales, el movimiento aburrido es escoger un dueño único para cada dato y eliminar los espejos.
Trampas comunes y cómo evitarlas
Almacenar cosas "por si acaso" es una de las formas más rápidas de hacer el estado doloroso, especialmente en apps generadas.
Copiar respuestas de la API en una store global es una trampa común. Si los datos vienen del servidor (listas, detalles, perfil de usuario), no los copies por defecto en una store cliente. Elige un hogar para los datos del servidor (normalmente la cache de queries). Usa la store cliente para valores de UI que el servidor no conoce.
Almacenar valores derivados es otra trampa. Contadores, listas filtradas, totales, canSubmit e isEmpty normalmente deben calcularse a partir de entradas. Si el rendimiento se convierte en un problema, memoiza más tarde, pero no empieces guardando el resultado.
Un mega-store único para todo (auth, modales, toasts, filtros, borradores, flags de onboarding) se vuelve un vertedero. Divide por límites de feature. Si el estado se usa solo en una pantalla, mantenlo local.
Context es genial para valores estables (tema, user id actual, locale). Para valores que cambian rápido, puede causar re-renders amplios. Usa Context para wiring, y estado de componente (o una store pequeña) para valores de UI que cambian frecuentemente.
Finalmente, evita nombres inconsistentes. Claves de query casi idénticas y campos de store duplicados crean duplicación sutil. Elige un estándar simple y síguelo.
Lista de comprobación rápida antes de añadir más estado
Cuando sientas la tentación de añadir "solo una variable de estado más", haz una verificación rápida de propiedad.
Primero, ¿puedes señalar un lugar donde suceden fetch y caching del servidor (una herramienta de queries, un conjunto de claves)? Si los mismos datos se obtienen en múltiples componentes y además se copian en una store, ya estás pagando intereses.
Segundo, ¿ese valor solo se necesita dentro de una pantalla (como "¿está abierto el panel de filtros?")? Si es así, no debería ser global.
Tercero, ¿puedes almacenar un ID en lugar de duplicar un objeto? Guarda selectedUserId y lee el usuario desde tu cache o lista.
Cuarto, ¿es derivado? Si puedes calcularlo desde el estado existente, no lo almacenes.
Finalmente, haz una prueba de trazado de un minuto. Si un compañero no puede responder "¿de dónde viene este valor?" (prop, estado local, cache del servidor, URL, store) en menos de un minuto, arregla la propiedad antes de añadir más estado.
Ejemplo: una app CRUD que se mantiene sencilla al crecer
Imagínate una app de administración generada (por ejemplo, una producida desde un prompt en Koder.ai) con tres pantallas: lista de clientes, página de detalle de cliente y un formulario de edición.
El estado se mantiene calmado cuando tiene hogares obvios:
- Estado servidor: lista de clientes, registro de cliente, requests de guardar/eliminar.
- Estado cliente: filtros de la lista, fila seleccionada, borrador de edición.
La lista y las páginas de detalle leen estado del servidor desde la cache de queries. Cuando guardas, no vuelves a almacenar clientes en una store global. Envías la mutación y dejas que la cache se refresque o la actualices.
Para la pantalla de edición, mantén el borrador del formulario local. Inicialízalo desde el cliente obtenido, pero trátalo como separado una vez que el usuario empiece a escribir. Así, la vista de detalle puede refrescarse sin sobrescribir cambios a medio hacer.
Una actualización optimista sencilla
La UI optimista es donde los equipos suelen duplicar todo. Normalmente no hace falta.
Cuando el usuario pulsa Guardar, actualiza solo el registro cliente cacheado y el elemento correspondiente de la lista, y revierte si la petición falla. Mantén el borrador en el formulario hasta que el guardado tenga éxito. Si falla, muestra un error y conserva el borrador para que el usuario reintente.
Cuando una segunda feature quiere el mismo estado
Supongamos que añades edición masiva y también necesita filas seleccionadas. Antes de crear una nueva store, pregúntate: ¿debe este estado sobrevivir a la navegación y al refresh?
Si sí, ponlo en la URL (IDs seleccionadas, filtros). Si no, mantenlo en el componente de la página. Si múltiples componentes distantes realmente lo necesitan al mismo tiempo (toolbar + tabla + footer), introduce una store compartida pequeña solo para ese estado cliente.
Próximos pasos: mantén tus reglas consistentes en apps generadas
Las pantallas generadas pueden multiplicarse rápido, y eso es genial hasta que cada nueva pantalla trae sus propias decisiones de estado.
Escribe una nota corta para el equipo en el repo: qué cuenta como estado servidor, qué cuenta como estado cliente y qué herramienta posee cada uno. Manténlo lo suficientemente corto para que la gente realmente lo siga.
Añade un pequeño hábito en los pull requests: etiqueta cada nueva pieza de estado como servidor o cliente. Si es estado servidor, pregunta "¿dónde se carga, cómo se cachea y qué lo invalida?". Si es estado cliente, pregunta "¿quién lo posee y cuándo se resetea?".
Si estás usando Koder.ai (koder.ai), Planning Mode puede ayudarte a acordar los límites de estado antes de generar nuevas pantallas. Un snapshot y rollback te dan una forma segura de experimentar cuando un cambio de estado sale mal.
Elige una característica (como editar perfil), aplica las reglas de principio a fin y deja que eso sea el ejemplo que todos copien.
Preguntas frecuentes
¿Cuál es la forma más simple de deshacer el lío con el estado en React?
Empieza por etiquetar cada pieza de estado como servidor, cliente (UI) o derivado.
- Servidor: datos obtenidos como usuarios, tareas, permisos.
- Cliente: elecciones de UI como “modal abierto”, pestaña seleccionada, texto de filtro.
- Derivado: valores que puedes calcular (lista filtrada, totales,
isValid).
Una vez etiquetados, asegúrate de que cada elemento tenga un dueño obvio (cache de consultas, estado local del componente, URL o una pequeña store).
¿Cómo sé si algo es estado del servidor o del cliente?
Usa esta prueba rápida: “¿Podría refrescar la página y reconstruir esto desde el servidor?”
- Si sí, trátalo como estado del servidor (fetch/cache/refetch).
- Si no, trátalo como estado del cliente (estado local de UI, quizás una store).
Ejemplo: una lista de proyectos es estado del servidor; el ID de la fila seleccionada es estado del cliente.
¿Por qué es tan problemático copiar datos de la API en estado local o global?
Porque crea dos fuentes de verdad.
Si obtienes users y luego los copias en useState o en una store global, ahora tienes que mantenerlos sincronizados durante:
- refetches en segundo plano
- actualizaciones desde otras pestañas/usuarios
- actualizaciones parciales tras mutaciones
Regla por defecto: lee los datos del servidor desde un solo lugar (la cache de consultas) y crea estado local solo para preocupaciones puramente de UI o borradores.
¿Cuándo está bien almacenar valores derivados en lugar de calcularlos?
Almacena valores derivados solo cuando realmente no puedes computarlos de forma barata.
Normalmente deberías calcularlos a partir de entradas existentes:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSaving
Si el rendimiento se convierte en un problema real (medido), prefiere useMemo o estructuras de datos mejores antes de introducir más estado almacenado que pueda quedar obsoleto.
¿Cuál es una manera “aburrida” de manejar el estado del servidor en React?
Por defecto: usa una herramienta de server-state (comúnmente TanStack Query) para que los componentes simplemente “pidan” datos y manejen loading/error.
Bases prácticas:
- Usa claves de consulta estables y coherentes (incluye solo las entradas que cambian el resultado).
- Para escrituras, usa mutations y elige una estrategia por característica:
- invalidar la query de lista/detalle exacta, o
- actualizar la cache de forma dirigida.
Evita esparcir llamadas refetch() por todos lados “por si acaso”.
¿Cuándo debería mover el estado de UI a una store global?
Mantenlo local hasta que puedas nombrar una necesidad real de compartir.
Regla de promoción:
- Estado local del componente por defecto.
- Eleva al padre común más cercano cuando los hermanos lo necesiten.
- Usa una pequeña store compartida solo cuando múltiples componentes/distantes o rutas necesiten el mismo valor al mismo tiempo.
Esto evita que tu store global se convierta en un vertedero de flags UI aleatorios.
¿Debo almacenar el objeto seleccionado o solo su ID?
Almacena IDs y flags pequeños, no objetos completos del servidor.
Ejemplo:
- ✅
selectedUserId - ❌
selectedUser(objeto copiado)
Luego renderiza detalles buscando el usuario en la lista/cache. Esto hace que los refetches en segundo plano y las actualizaciones funcionen correctamente sin código extra de sincronización.
¿Cuál es la forma más limpia de manejar formularios y borradores?
Trata el formulario como un borrador (estado cliente) hasta que lo envíes.
Patrón práctico:
- Obtén el registro guardado como estado del servidor.
- Inicializa un borrador local a partir de él.
- Permite que el usuario edite el borrador libremente.
- Al guardar, envía el borrador y luego refresca/actualiza la cache del servidor.
- Al cancelar, descarta el borrador y muestra la versión del servidor.
Esto evita editar datos del servidor “in situ” y pelear con refetches.
¿Cuáles son las señales tempranas de que la complejidad de estado va a explotar?
Señales comunes:
- Los mismos datos existen en un componente, una store global y una cache de queries.
- Efectos que reflejan estado de un lado a otro (“cuando la query cambia, actualiza la store”; “cuando la store cambia, refetch”).
- Flags compartidos que se acumulan (
needsRefresh,didInit,isSaving). - No puedes explicar “¿de dónde viene este valor?” en una sola frase.
La solución suele ser eliminar duplicados y elegir un dueño por cada valor, no una librería nueva.
¿Cómo mantengo el estado “sencillo” cuando uso un generador como Koder.ai?
Estandariza la propiedad:
- Estado servidor: siempre vía la misma capa de fetching/caching.
- Estado UI cliente: local por defecto; store solo cuando sea realmente necesario.
- Derivados: calculados, no almacenados.
Si usas Koder.ai, usa Planning Mode para decidir la propiedad antes de generar pantallas y apóyate en snapshots/rollback si experimentas con cambios de estado.