Estados de UI: piensa en “qué se ve” según el estado
El renderizado condicional en React consiste en decidir qué JSX se devuelve en función del estado actual de la interfaz. En la práctica, casi todas las pantallas caen en un conjunto pequeño de estados de UI: cargando, error, vacío (no hay datos) y éxito (hay datos). La clave es que estos estados sean explícitos y mutuamente excluyentes para evitar combinaciones incoherentes (por ejemplo, mostrar “cargando” y “error” a la vez).
Modelo mental recomendado
- Cargando: todavía no tengo respuesta o estoy recalculando algo.
- Error: falló la operación; puedo mostrar un mensaje y una acción (reintentar).
- Vacío: la operación fue exitosa, pero no hay elementos que mostrar.
- Éxito: tengo datos válidos para renderizar el contenido principal.
En vez de dispersar condiciones por toda la UI, intenta que el componente tenga una ruta clara para cada estado.
Operadores y patrones frecuentes: &&, ternario y early return
1) Operador &&: para “mostrar algo si se cumple”
Útil cuando solo necesitas renderizar un bloque si una condición es verdadera y no hay alternativa.
function UserBadge({ isOnline }) { return ( <div> <span>Usuario</span> {isOnline && <span>En línea</span>} </div> );}Pauta: evita usar && con valores que puedan ser 0 o cadena vacía si eso es un caso válido, porque React renderiza 0 como texto. En esos casos, convierte a booleano: Boolean(value) && ....
2) Ternario: para “A o B”
Útil cuando hay dos ramas claras y cortas.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
function SaveButton({ isSaving }) { return ( <button disabled={isSaving}> {isSaving ? "Guardando..." : "Guardar"} </button> );}Pauta: si el ternario crece (más de 1–2 líneas por rama) o se encadena, suele perder legibilidad. En ese caso, considera extraer a variables o usar early return.
3) Early return: para estados de pantalla completos
Es el patrón más legible cuando cada estado reemplaza prácticamente toda la UI (por ejemplo, una página o un panel principal).
function UsersPanel({ status, users, error, onRetry }) { if (status === "loading") { return <div>Cargando usuarios...</div>; } if (status === "error") { return ( <div> <p>No se pudieron cargar los usuarios.</p> <button onClick={onRetry}>Reintentar</button> <pre>{String(error)}</pre> </div> ); } if (status === "success" && users.length === 0) { return <div>No hay usuarios todavía.</div>; } return ( <ul> {users.map((u) => ( <li key={u.id}>{u.name}</li> ))} </ul> );}Cuándo elegir cada uno
| Patrón | Úsalo cuando | Evítalo cuando |
|---|---|---|
&& | Solo hay “mostrar/no mostrar” | Hay alternativa importante o la condición no es claramente booleana |
| Ternario | Dos ramas cortas y simétricas | Ramas largas o ternarios encadenados |
| Early return | Estados de pantalla/panel completos | Solo cambian detalles pequeños dentro del mismo layout |
Guía práctica paso a paso: de condiciones dispersas a estados claros
Paso 1: define un “status” único y explícito
En lugar de múltiples booleanos (isLoading, hasError, isEmpty), define un estado único que represente el estado dominante de la UI.
// Ejemplo de valores posibles: "idle" | "loading" | "error" | "success"const status = "loading";Esto reduce combinaciones imposibles (por ejemplo, isLoading y hasError a la vez) y hace el render más predecible.
Paso 2: deriva “vacío” desde los datos, no como bandera separada
“Vacío” suele ser un caso de éxito sin elementos. En vez de guardar isEmpty en estado, calcúlalo:
const isEmpty = status === "success" && items.length === 0;Paso 3: aplica early return para estados globales
Si el estado cambia el contenido principal, usa early return. Mantén el “camino feliz” (éxito con datos) al final.
function ProductsView({ status, products, error }) { if (status === "loading") return <ProductsSkeleton />; if (status === "error") return <ErrorState message="Falló la carga" details={error} />; if (status === "success" && products.length === 0) return <EmptyState />; return <ProductsGrid products={products} />;}Paso 4: evita condiciones anidadas con “guardas” (guards)
Una guarda es una condición que corta el render temprano o evita ejecutar lógica cuando faltan datos. Esto previene errores como intentar leer propiedades de null o renderizar UI incompleta.
function ProfileCard({ user }) { if (!user) return null; // guarda: sin usuario, no hay tarjeta return ( <section> <h3>{user.name}</h3> <p>{user.email}</p> </section> );}Pauta: si devolver null oculta demasiado, considera un placeholder explícito:
if (!user) return <div>Selecciona un usuario</div>;Cómo evitar condiciones anidadas difíciles de mantener
Problema típico: “pirámide” de if/ternarios
// Evitarfunction Panel({ status, data }) { return ( <div> {status === "loading" ? ( <Spinner /> ) : status === "error" ? ( <ErrorState /> ) : data.length === 0 ? ( <EmptyState /> ) : ( <List data={data} /> )} </div> );}Alternativas más legibles
- Early return (recomendado para estados de pantalla).
- Extraer a variables cuando el layout externo es común.
- Tabla de decisión (mapa de estado → componente) si el estado es un string/enum.
Extraer a variable manteniendo layout consistente
Si quieres conservar un contenedor común (mismo padding, header, etc.), calcula el contenido en una variable.
function Panel({ status, items, error }) { let content = null; if (status === "loading") content = <Spinner />; else if (status === "error") content = <ErrorState details={error} />; else if (items.length === 0) content = <EmptyState />; else content = <List items={items} />; return ( <section className="panel"> <header>Resultados</header> <div className="panel-body">{content}</div> </section> );}Tabla de decisión (mapa) para estados simples
const viewByStatus = { loading: <Spinner />, error: <ErrorState />};function Panel({ status, items }) { if (status in viewByStatus) return viewByStatus[status]; if (items.length === 0) return <EmptyState />; return <List items={items} />;}Pauta: usa este enfoque cuando las vistas no necesitan props específicas. Si necesitan props, mapea a funciones:
const viewByStatus = { loading: () => <Spinner />, error: (error) => <ErrorState details={error} />};UI consistente: evita “saltos” y estados contradictorios
1) Mantén el layout estable durante la carga
Si al cargar desaparece todo y luego aparece, la UI “salta”. Prefiere mantener el contenedor y cambiar el contenido interno, o usar skeletons con dimensiones similares al contenido final.
function ProductsSkeleton() { return ( <div> <div className="skeleton-title" /> <div className="skeleton-grid" /> </div> );}2) No mezcles “error” con “éxito” sin una regla clara
Si tienes datos previos y ocurre un error al refrescar, decide explícitamente: ¿muestras los datos antiguos con un aviso, o reemplazas por un error? Define una regla y aplícala siempre.
// Ejemplo de regla: si hay datos previos, se muestran y el error va como avisoif (status === "error" && products.length > 0) { return ( <> <InlineWarning message="No se pudo actualizar. Mostrando datos anteriores." /> <ProductsGrid products={products} /> </> );}3) Distingue “vacío” de “sin permiso” o “sin búsqueda”
“Vacío” no siempre significa “no hay nada”. A veces significa “todavía no buscaste” o “no tienes permisos”. Trata esos casos como estados distintos para no confundir al usuario.
Estructura de componentes para estados de carga predecibles
Una forma práctica de mantener consistencia es separar: (1) el componente que obtiene/recibe el estado, (2) los componentes de estado (Loading/Error/Empty), y (3) el componente de éxito.
Opción A: Componente contenedor de estados (Stateful wrapper)
function DataBoundary({ status, error, isEmpty, onRetry, children }) { if (status === "loading") return <LoadingState />; if (status === "error") return <ErrorState details={error} onRetry={onRetry} />; if (isEmpty) return <EmptyState />; return children;}function UsersSection({ status, users, error, onRetry }) { return ( <DataBoundary status={status} error={error} onRetry={onRetry} isEmpty={status === "success" && users.length === 0} > <UsersList users={users} /> </DataBoundary> );}Ventaja: estandarizas el orden de prioridad (loading > error > empty > success) y reduces duplicación.
Opción B: Componente “Switch” explícito por estado
Si prefieres que cada estado sea una rama declarativa, puedes crear un componente que reciba “slots” por estado.
function UISwitch({ status, loading, error, empty, success }) { if (status === "loading") return loading; if (status === "error") return error; if (status === "empty") return empty; return success;}function OrdersSection({ status, orders }) { const derivedStatus = status === "success" && orders.length === 0 ? "empty" : status; return ( <UISwitch status={derivedStatus} loading={<LoadingState />} error={<ErrorState />} empty={<EmptyState />} success={<OrdersTable orders={orders} />} /> );}Pauta: si introduces un estado derivado como empty, hazlo en un solo lugar (no repetido en múltiples componentes).
Depurar renderizados inesperados: checklist y técnicas
1) Identifica qué condición está activando la rama
Cuando la UI muestra una rama “equivocada”, el problema suele ser que el estado no está en el valor esperado o que la condición está escrita de forma ambigua.
- Inspecciona el valor real de
status,errory la longitud de los datos. - Verifica el orden de prioridad: si
loadingse evalúa antes queerror, podrías ocultar errores.
console.log({ status, hasError: Boolean(error), count: items?.length });2) Cuidado con valores “truthy/falsy”
Errores comunes:
[]es truthy:items && <List />renderiza aunque esté vacío.0es falsy:count && <Badge />no renderiza cuando count es 0 (quizá sí quieres mostrar 0).
// Mejor: condiciones explícitas{items.length > 0 && <List items={items} />}{count !== null && count !== undefined && <Badge>{count}</Badge>}3) Verifica que no estés renderizando con datos “viejos”
Si actualizas datos y ves un estado anterior, revisa:
- Si el estado de carga se activa al iniciar la petición.
- Si el estado de éxito/error se actualiza al resolver/rechazar.
- Si estás derivando “vacío” de datos que aún no se han actualizado.
4) Usa React DevTools para ver props/estado en tiempo real
Selecciona el componente y observa cómo cambian sus valores al interactuar. Si el componente se renderiza “demasiado”, revisa qué valores cambian y si estás recreando objetos/arrays innecesariamente (por ejemplo, props derivadas en cada render).
5) Añade marcas de render para confirmar el flujo
Para entender el orden de renders y qué rama se ejecuta:
function Panel({ status }) { console.log("Render Panel", status); if (status === "loading") { console.log("Rama: loading"); return <Spinner />; } console.log("Rama: success"); return <div>OK</div>;}Pauta: elimina estos logs cuando termines; si necesitas algo más robusto, centraliza el diagnóstico en un helper o en un componente boundary de estados.