Renderizado condicional
Muestra, oculta o intercambia la UI según el estado, las props o valores calculados.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
// Ternario — elige entre dos elementos
{isLoggedIn ? <Dashboard /> : <LoginForm />}
// AND lógico — muestra o nada
{hasNotifications && <Badge count={notifications.length} />}
// Retorno temprano — sal de todo el componente
if (!user) return <Skeleton />;
return <Profile user={user} />;
// Variable extraída — legible cuando la lógica es compleja
const content = status === "loading"
? <Spinner />
: status === "error"
? <ErrorMessage />
: <DataTable rows={data} />;
return <section>{content}</section>;Cuándo recurrir a esto: Siempre que la UI cambie según una condición — estados de carga, controles de autenticación, feature flags, estados vacíos.
Ejemplo práctico
"use client";
import { useState } from "react";
type Status = "idle" | "loading" | "success" | "error";
interface FetchResult {
status: Status;
data?: string[];
error?: string;
}
export function FetchDemo() {
const [result, setResult] = useState<FetchResult>({ status: "idle" });
async function handleFetch() {
setResult({ status: "loading" });
try {
// Simula una petición de red
await new Promise(resolve => setTimeout(resolve, 1000));
const success = Math.random() > 0.3;
if (!success) throw new Error("Network timeout");
setResult({
status: "success",
data: ["React", "Next.js", "TypeScript"],
});
} catch (err) {
setResult({
status: "error",
error: err instanceof Error ? err.message : "Unknown error",
});
}
}
return (
<div className="max-w-sm space-y-4 rounded border p-4">
<button
onClick={handleFetch}
disabled={result.status === "loading"}
className="rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
{result.status === "loading" ? "Cargando..." : "Obtener datos"}
</button>
{result.status === "idle" && (
<p className="text-gray-500">Pulsa el botón para cargar los datos.</p>
)}
{result.status === "loading" && (
<div className="animate-pulse rounded bg-gray-200 p-4">Cargando...</div>
)}
{result.status === "error" && (
<div className="rounded bg-red-50 p-3 text-red-700">
<strong>Error:</strong> {result.error}
</div>
)}
{result.status === "success" && result.data && (
<ul className="list-inside list-disc">
{result.data.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}Lo que esto demuestra:
- Una unión discriminada (
Status) que impulsa cuatro estados de UI distintos - El operador
&¶ renderizar cada rama de estado de forma independiente - Un botón deshabilitado con texto de etiqueta condicional
- Acotamiento de tipos — se accede a
result.datade forma segura solo en la rama"success"
Análisis a fondo
Cómo funciona
- React renderiza lo que sea que devuelva la función del componente — no hay una sintaxis de plantilla especial para los condicionales
false,null,undefinedytrueson hijos válidos de JSX que no renderizan nada, razón por la cual funcionan&&y los ternarios- Cuando un condicional cambia y el tipo de componente en una posición cambia (p. ej., de
<Spinner />a<DataTable />), React desmonta el árbol antiguo y monta uno nuevo - Cuando el tipo de componente permanece igual pero las props cambian, React actualiza en su lugar
Comparación de patrones
| Patrón | Ideal para | Ejemplo |
|---|---|---|
Ternario ? : | Elegir entre dos elementos | {ok ? <A /> : <B />} |
&& | Mostrar u ocultar un elemento | {show && <Modal />} |
| Retorno temprano | Proteger todo el componente | if (!data) return null; |
| Extracción a variable | Lógica compleja con varias ramas | const ui = ...; |
| Objeto de mapa/búsqueda | Muchos estados discretos | statusMap[status] |
Patrón de objeto de búsqueda
const statusUI: Record<Status, React.ReactNode> = {
idle: <p>Waiting...</p>,
loading: <Spinner />,
success: <DataTable />,
error: <ErrorBanner />,
};
return <div>{statusUI[status]}</div>;Notas sobre TypeScript
// Las uniones discriminadas hacen que el renderizado condicional sea seguro en tipos
type Result =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: string };
function Display({ result }: { result: Result }) {
switch (result.status) {
case "idle":
return <p>Ready</p>;
case "loading":
return <Spinner />;
case "success":
// result.data se acota a string[] aquí
return <List items={result.data} />;
case "error":
// result.error se acota a string aquí
return <Alert message={result.error} />;
}
}Trampas
-
0 &&renderiza un0literal —{count && <Badge />}muestra0en pantalla cuando count es cero porque0es un valor falsy pero renderizable. Solución: Usa{count > 0 && <Badge />}o un ternario. -
Falta el retorno de
null— Un componente que devuelve JSX de forma condicional pero no tiene un retorno de respaldo devuelve implícitamenteundefined, lo cual funciona pero provoca advertencias del linter. Solución: Devuelve explícitamentenullpara el caso vacío. -
Reinicio del estado al cambiar de tipo — Alternar entre
<input type="text" />y<textarea />en la misma posición del árbol destruye el estado porque son tipos de elemento diferentes. Solución: Si necesitas preservar el estado, renderiza ambos y alterna la visibilidad con CSS, o eleva el estado. -
Ternarios anidados —
{a ? b ? <X /> : <Y /> : <Z />}es difícil de leer y fácil de interpretar mal. Solución: Extrae a una variable o usa un patrón de switch/mapa.
Alternativas
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
CSS display: none | Quieres mantener el componente montado y preservar su estado (p. ej., paneles de pestañas) | El contenido oculto es costoso de renderizar o implica peticiones de red |
<Suspense> + lazy | Cargas condicionalmente un chunk completo separado por código | Un simple mostrar/ocultar dentro de un componente ya cargado |
| Renderizado basado en rutas | Diferentes "páginas" que cada una tiene su propia URL | Alternar dentro de una sola vista |
Ejemplo del mundo real
De una aplicación SaaS de producción con Next.js 15 / React 19 (SystemsArchitect.io).
// Ejemplo de producción: visualización de banners con múltiples cláusulas de guarda
// Archivo: src/components/banner/banner-display.tsx
export default function BannerDisplay({ location, className }: BannerDisplayProps) {
const { banners, loading, error, fetchBanners } = useBannerStore();
const locationBanners = banners[location] || [];
useEffect(() => {
fetchBanners(location);
}, [location]);
// Cláusula de guarda 1: cargando sin datos en caché
if (loading && locationBanners.length === 0) {
return null;
}
// Cláusula de guarda 2: estado de error
if (error) {
return null;
}
// Cláusula de guarda 3: no hay banners para esta ubicación
if (locationBanners.length === 0) {
return null;
}
// Camino feliz: renderiza los banners
return (
<div className={cn('space-y-4', className)}>
{locationBanners.map((banner) => (
<div
key={banner.id}
className={cn(
'rounded-md py-2 px-4 shadow-sm',
getBorderClass(banner.borderStyle),
'bg-zinc-50 dark:bg-zinc-900',
banner.customStyles
)}
>
{banner.content}
</div>
))}
</div>
);
}Lo que esto demuestra en producción:
- Tres sentencias tempranas de
return nullactúan como cláusulas de guarda, evitando el renderizado cuando no hay nada útil que mostrar loading && locationBanners.length === 0significa no mostrar nada durante la primera carga, pero seguir mostrando banners obsoletos durante una actualizaciónbanners[location] || []proporciona un respaldo seguro para que.lengthnunca falle sobre undefined- La utilidad
cn()(clsx + tailwind-merge) une nombres de clase de forma condicional - Decisión de diseño: devolver null en lugar de un skeleton evita el desplazamiento del layout para UI opcional como los banners
Preguntas frecuentes
¿Cuál es la mejor forma de renderizar un componente de forma condicional?
Usa un ternario {condition ? <A /> : <B />} para elegir entre dos elementos, o {condition && <A />} para mostrar u ocultar un elemento. Para lógica compleja con varias ramas, usa una variable u objeto de búsqueda antes del return.
¿Por qué 0 && Component renderiza "0" en pantalla?
El operador && de JavaScript devuelve el primer valor falsy. 0 es falsy pero es un hijo válido de React que se renderiza como texto. Usa {count > 0 && <Component />} o un ternario en su lugar.
¿Cuándo debería usar el retorno temprano frente al ternario?
- Retorno temprano para proteger todo el componente (estados de carga, error, no encontrado)
- Ternario para cambios en línea dentro del árbol JSX
- Los retornos tempranos son más legibles cuando hay múltiples condiciones de guarda
¿Qué es el patrón de objeto de búsqueda para el renderizado condicional?
Mapea valores de estado a elementos JSX usando un Record:
const statusUI: Record<Status, React.ReactNode> = {
idle: <p>Waiting...</p>,
loading: <Spinner />,
success: <DataTable />,
error: <ErrorBanner />,
};
return <div>{statusUI[status]}</div>;¿El renderizado condicional desmonta y vuelve a montar los componentes?
Sí, si el tipo de componente en una posición del árbol cambia (p. ej., de <Spinner /> a <DataTable />), React desmonta el componente antiguo y monta uno nuevo, destruyendo todo el estado. Si solo cambian las props en el mismo tipo de componente, React actualiza en su lugar.
¿Cómo preservo el estado al alternar entre dos vistas?
- Renderiza ambas y alterna la visibilidad con CSS
display: none - Eleva el estado a un componente padre que persista en ambas vistas
- Usa un almacén de estado compartido como Zustand
¿Qué es una unión discriminada y cómo ayuda al renderizado condicional?
Una unión de TypeScript donde cada miembro tiene un campo status literal. El switch o if sobre status acota el tipo, de modo que TypeScript sabe qué campos están disponibles en cada rama — eliminando las comprobaciones de undefined.
¿Es malo usar ternarios anidados?
Sí, son difíciles de leer. Para más de dos ramas, extrae la lógica a una variable, usa una sentencia switch o usa el patrón de objeto de búsqueda.
¿Cuándo debería usar CSS display:none frente al renderizado condicional?
- Usa
display: nonecuando quieras mantener el componente montado y preservar su estado (p. ej., paneles de pestañas) - Usa el renderizado condicional cuando el contenido oculto sea costoso de renderizar, obtenga datos o ejecute efectos que quieras detener
¿Cómo renderizo nada desde un componente?
Devuelve null. Esto es más limpio que devolver un Fragment vacío <></> y es el patrón estándar para componentes que condicionalmente no tienen salida.
¿Puedo usar sentencias switch en JSX?
No directamente dentro de JSX. Extráelo a una variable o a una función auxiliar:
function getStatusIcon(status: Status) {
switch (status) {
case "success": return <CheckIcon />;
case "error": return <XIcon />;
default: return null;
}
}
return <div>{getStatusIcon(status)}</div>;Relacionado
- JSX y TSX — ranuras de expresión donde viven los condicionales
- Listas y claves — renderizado de colecciones dinámicas
- Componentes — estructurar ramas condicionales como componentes separados