Diseño de pantallas en React para estados de carga, vacío, error y reintentos
Aprenda a modelar cada estado en el que puede encontrarse una pantalla y un formulario de React, desde estructuras básicas y actualizaciones, hasta resultados vacíos, errores útiles y protección contra envíos duplicados.
Una pantalla que se muestra perfectamente con una base de datos local ya preparada puede seguir fallando en entornos de producción, donde los usuarios se enfrentan a redes lentas, tablas vacías y puntos finales que no funcionan. Esta guía explica los estados por los que pasan realmente una pantalla de React basada en datos y sus formularios, y le proporciona una lista de verificación para diseñar cada uno antes de escribir cualquier JSX.
El modelo mental detrás de la mayoría de las pantallas frágiles
Cuando se esboza por primera vez un componente, el flujo tiende a parecer una línea recta: solicitar algo y luego renderizarlo.
Fetch data → Show data
Esa imagen no deja espacio para fallos. Una pantalla en producción tiene ramificaciones: comienza a cargarse, puede terminar con éxito o con un resultado vacío, puede producirse un error y necesita una forma de recuperarse mediante intentos repetidos o recargas.
Loading → Success → Empty
↓
Error
↓
Retry / Refetch
Tratar la carga como un estado real con su propio diseño
“Mostrar un spinner” suena como una respuesta completa, al igual que “envolverlo en try/catch”: no está mal, pero rara vez abarca lo que experimentan los usuarios. Dos preguntas revelan esa brecha: ¿cómo se ve la pantalla cuando la respuesta tarda cuatro segundos? ¿Y qué ocurre cuando llega en 40 milisegundos: el usuario ve un spinner parpadeando un solo frame y considera que la interfaz es inestable?
Carga inicial versus recarga
Es útil dividir la carga en dos situaciones distintas:
- Carga inicial. Todavía no hay nada en la pantalla, por lo que un esqueleto que refleje el diseño final es una opción razonable e incluso agradable.
- Recarga. Los datos ya son visibles, por ejemplo después de que el usuario pulse “actualizar” o cambie un filtro. Borrarlos y reemplazarlos por un cargador de página completa pierde contexto útil.
Durante una nueva carga, mantenga los datos anteriores mostrados y coloque un pequeño indicador integrado cerca del elemento que se está actualizando. Los spinners no tranquilizan a nadie; en el mejor de los casos se toleran. Para respuestas muy rápidas, considere retrasar brevemente la aparición del indicador para que solo se muestre cuando la espera sea perceptible.
Por qué una llamada fetch funcional es solo una pequeña parte del trabajo
La solicitud en sí es la parte sencilla:
const res = await fetch("/api/students");
const data = await res.json();
setStudents(data);
Lo que este fragmento no responde es todo lo relacionado con él. ¿Qué ve el usuario mientras la promesa está pendiente? ¿Qué pasa si fetch rechaza la solicitud, o el servidor responde con un estado distinto a 2xx (que fetch por sí mismo no considera un error)? ¿Y si el cuerpo de la respuesta es un array vacío? La pregunta útil no es cómo obtener los datos, sino en qué estados puede encontrarse esta solicitud en cualquier momento.
Si students es el único elemento de estado, el resto de la interfaz debe deducir la situación a partir de si esa variable es null, está vacía o contiene datos. null podría significar “todavía cargando” o “la solicitud falló”, y no hay forma de distinguirlos. Eso es adivinar disfrazado de gestión de estado.
Un resultado vacío es un éxito, no un error
Vale la pena destacar este punto porque muchos conjuntos de código lo entienden erróneamente. Una respuesta 200 OK que contiene [] indica que el servidor está funcionando correctamente: está informando que no hay nada que mostrar.
“No se encontraron estudiantes” y “No se pudo cargar a los estudiantes” describen situaciones completamente diferentes, sin embargo, muchas aplicaciones muestran el mismo marcador gris para ambas. Un <p>Algo salió mal</p> común para ambos casos combina dos situaciones que requieren soluciones distintas: un estado vacío podría invitar al usuario a eliminar un filtro, mientras que un estado de error debería explicar la falla y ofrecer una forma de recuperarse.
Así debería verse un estado de error útil
El mensaje genérico “Algo salió mal” no ayuda en nada. Un mejor estado de error:
- informa al usuario sobre algo que puede hacer, como verificar su conexión o intentarlo de nuevo más tarde;
- ofrece un botón para intentar nuevamente cuando haya posibilidades reales de que funcione;
- nunca muestra en la interfaz un rastro de errores bruto ni datos internos relacionados con el error;
- se mantiene dentro de la zona de la pantalla donde ocurrió la falla.
El último elemento es el que más a menudo se omite. En un panel de control con cinco widgets, una solicitud fallida no debería hacer que desaparezcan los cuatro que se cargaron con éxito. El manejo de errores por sección, a través del estado local o de límites de error alrededor de regiones independientes.
Los formularios siguen su propio ciclo de vida
Escribir datos tiene aún más estados que leerlos:
Idle → Editing → Submitting → Success / Error
Protección contra envíos duplicados
Considere un usuario que hace clic en Enviar tres veces porque el botón no mostró ninguna retroalimentación visible después de los dos primeros clics. A menos que el botón esté deshabilitado durante el envío, o que el manejador verifique una bandera isSubmitting y regrese temprano cuando ya hay una solicitud en trámite, el servidor recibirá tres solicitudes. En el caso de un formulario de pedido, eso significa que se facturará a un cliente tres veces por una sola camiseta. En cuanto a los pagos, el backend también debe eliminar las duplicaciones, por ejemplo mediante una clave de idempotencia.
Mantener la entrada del usuario cuando falla el envío
Cuando la solicitud falla, deje el formulario lleno. Pedirle a alguien que vuelva a escribir un mensaje largo después de un problema de red es una pérdida de tiempo. Los errores de validación, ya provengan de reglas del lado del cliente o del servidor, deben aparecer junto al campo al que se refieren y no en un banner vago en la parte superior del formulario.
Sustituir booleanos dispersos por estados nombrados
Si no se controlan, un componente tiende a acumular un conjunto de indicadores como estos:
isLoading, isError, isSubmitting, hasData, isEmpty
Cada indicador tiene sentido por sí solo. En conjunto, cinco booleanos permiten 32 combinaciones, de las cuales solo unas pocas son significativas, y nada impide que isLoading && isError sea verdadero en ambos casos.
Una página de lista de estudiantes no necesita una biblioteca formal de máquinas de estado. Lo que sí requiere es una decisión deliberada, tomada antes de la primera llamada a useState, sobre qué estados distintos puede ocupar la pantalla. Represente el estado actual con un único valor, como un campo status que contenga "loading", "success", "empty" o "error", y deje que el renderizado se base en ese valor en lugar de reconstruir la situación a partir de varias banderas independientes. En TypeScript, una unión discriminada impide representar combinaciones imposibles; la publicación en sobre el uso de useState, useReducer y hooks personalizados aborda en mayor profundidad el aspecto de tipado.
Una lista de verificación para ejecutar antes de crear la siguiente pantalla
Responda a estas preguntas antes de escribir el return del componente:
- ¿Qué es lo que se muestra antes de que llegue cualquier dato?
- ¿Qué sucede cuando la solicitud falla?
- ¿Qué ocurre cuando tiene éxito pero no devuelve nada?
- ¿Qué ve el usuario mientras se actualizan los datos existentes?
- ¿Qué pasa durante el envío de un formulario?
- ¿Qué sucede si se envía el formulario dos veces?
- ¿Qué ocurre cuando falla la validación?
Conclusión
La diferencia entre una demostración e una interfaz de producción rara vez radica en el escenario ideal. Se encuentra en los estados de carga, vacío, error, actualización y envío que aparecen en cuanto entran en juego usuarios reales y redes reales. Asigne nombres explícitos a esos estados, diseñe cada uno de manera deliberada, mantenga las fallas localizadas y proteja la entrada del usuario y los efectos en el servidor de envíos repetidos o fallidos.
Lecturas relacionadas
- UI optimista sin desincronizaciones: snapshots, retrocesos y peticiones anuladas — Aprenda a crear actualizaciones optimistas que mantengan la corrección: estado de snapshot, actualización instantánea, retroceso en caso de fallo, cancelación de peticiones obsoletas y cómo saber cuándo no utilizar este patrón.
- Estructurando una capa de datos TanStack Query, desde queryOptions hasta retrocesos — Construya una capa de datos TanStack Query paso a paso: queryOptions compartidos, generadores de claves, selectores, paginación, precarga, invalidación centralizada y actualizaciones optimistas seguras.