Renderizado condicional
Muestra, oculta o intercambia la UI según el estado, las props o valores calculados.
Busca en todas las páginas de la documentación
Muestra, oculta o intercambia la UI según el estado, las props o valores calculados.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
Status) que impulsa cuatro estados de UI distintos&& para renderizar cada rama de estado de forma independienteresult.data de forma segura solo en la rama "success"false, null, undefined y true son hijos válidos de JSX que no renderizan nada, razón por la cual funcionan && y los ternarios<Spinner /> a <DataTable />), React desmonta el árbol antiguo y monta uno nuevo| 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] |
const statusUI: Record<Status, React.ReactNode> = {
idle: <p>Waiting...</p>,
loading: <Spinner />,
success: <DataTable />,
error: <ErrorBanner />,
};
return <div>{statusUI[status]}</div>;// 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} />;
}
}0 && renderiza un 0 literal - {count && <Badge />} muestra 0 en pantalla cuando count es cero porque 0 es 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ícitamente undefined, lo cual funciona pero provoca advertencias del linter. Solución: Devuelve explícitamente null para 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.
| 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 |
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:
return null actúan como cláusulas de guarda, evitando el renderizado cuando no hay nada útil que mostrarloading && locationBanners.length === 0 significa no mostrar nada durante la primera carga, pero seguir mostrando banners obsoletos durante una actualizaciónbanners[location] || [] proporciona un respaldo seguro para que .length nunca falle sobre undefinedcn() (clsx + tailwind-merge) une nombres de clase de forma condicionalUsa 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.
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.
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>;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.
display: noneUna 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.
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.
display: none cuando quieras mantener el componente montado y preservar su estado (p. ej., paneles de pestañas)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.
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>;Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥