Condiciones: vacío, error, cargando
En resumen: El renderizado condicional muestra solo la parte del marcado que corresponde a los datos (cargando, error, vacío o la lista); en React con && y el operador ternario.
Problema
Cargando, error y la lista a la vez
El catálogo se ha cargado. Pero bajo el encabezado dice «Cargando…», luego «Los productos no se cargaron.», luego «No hay productos en esta categoría.», y solo después vienen los productos.
Consola
El componente Catalog recibe status y los productos como props. Pero no los mira: siempre dibuja las cuatro partes. El comprador no sabe qué creer, y un lector de pantalla lee tanto el error como los productos.
La idea
La parte que corresponde a los datos
El catálogo tiene cuatro estados: cargando, error, vacío y listo. En la pantalla debe estar solo la parte del estado actual. Esto es el renderizado condicional: el componente decide qué devolver según los datos.
JSX tiene tres maneras, según cuánto cambie:
// Una vista completamente distinta: if y un return tempranoif (status === 'loading') return <p role="status">Cargando…</p>;
// Una de dos: ? :<p>{product.stock === 0 ? 'Agotado' : 'Disponible'}</p>
// Algo o nada: &&{status === 'error' && <p role="alert">Los productos no se cargaron.</p>}if es JavaScript normal: el componente es una función y puede devolver distinto JSX. ? : y && son expresiones, así que pueden estar dentro de JSX, en {}.
Las dos bibliotecas resuelven lo mismo a su manera: React con JavaScript alrededor del marcado, Vue con atributos dentro. El resultado es el mismo: el elemento de una condición falsa no está en el DOM. No está oculto, no existe.
Aquí status se pasa como prop. Cómo lo cambia el propio catálogo mientras carga desde la API está en el módulo «Efectos, datos y formularios».
Míralo
Ver qué rama entra
Un valor entra en el componente, y solo una rama entra en el DOM. Fíjate en la columna «DOM» en cada cambio de status.
function Catalog({ status, products }) { if (status === 'loading') return <p>Cargando…</p>; if (status === 'error') return <p>Error</p>; if (status === 'empty') return <p>No hay productos</p>; return <ul>…</ul>;}
// en el DOM, pero oculto<p hidden={status !== 'loading'}>Cargando…</p>Paso a paso, en palabras
- status llega al componente como prop. Es 'loading': el primer if coincide, y el componente devuelve solo <p>Cargando…</p>. En el DOM entra solo eso.
- status pasa a 'error'. La biblioteca dibuja otra vez: ahora coincide la segunda rama. <p>Cargando…</p> sale del DOM, y entra el mensaje de error.
- 'empty': solo el mensaje de que la categoría está vacía.
- 'ready': ningún if coincide, y el componente llega al último return: la lista. En el DOM hay siempre una rama.
- La otra manera: el elemento se dibuja siempre, y el atributo hidden decide si se ve. Con loading, hidden es false y el texto se ve.
- status pasa a 'ready'. El elemento se queda en el DOM, con todo lo que tiene dentro, y solo recibe display: none. Nadie lo ve, y un lector de pantalla lo salta.
- Una condición con if, ? : o && quita el elemento del DOM. hidden solo lo oculta. Quitar es lo habitual; ocultar es para algo que se cambia a menudo o que debe conservar lo escrito.
La primera parte es el renderizado condicional: con un valor nuevo, el elemento viejo sale del DOM y entra el nuevo. La segunda parte es ocultar: el elemento está en el DOM todo el tiempo y solo se muestra o se oculta. De eso trata el final de la lección.
Pruébalo
Los cuatro estados
Catalog dibuja todo a la vez. Haz que cada sección tenga solo la parte de su estado: «Cargando…» para cargando, el error para error, «No hay productos en esta categoría.» para vacío y la lista para listo. Las otras partes no deben estar en la página.
Consola
Solución
Catalog.jsximport ProductCard from './ProductCard';
export default function Catalog({ status, products }) {
if (status === 'loading') return <p role="status">Cargando…</p>;
if (status === 'error') return <p role="alert">Los productos no se cargaron.</p>;
if (status === 'empty') return <p>No hay productos en esta categoría.</p>;
return (
<ul>
{products.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
);
}
App.jsximport Catalog from './Catalog';
const PRODUCTS = [
{ id: 1, name: 'Pan blanco', price: 1.2, stock: 5 },
{ id: 3, name: 'Queso blanco', price: 7.9, stock: 0 },
{ id: 2, name: 'Leche fresca', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<section aria-label="Cargando">
<h2>Cargando</h2>
<Catalog status="loading" products={[]} />
</section>
<section aria-label="Error">
<h2>Error</h2>
<Catalog status="error" products={[]} />
</section>
<section aria-label="Categoría vacía">
<h2>Categoría vacía</h2>
<Catalog status="empty" products={[]} />
</section>
<section aria-label="Listo">
<h2>Listo</h2>
<Catalog status="ready" products={PRODUCTS} />
</section>
</main>
);
}
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
<button type="button" aria-label={`Añadir ${product.name}`}>Añadir</button>
</article>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
if con un return temprano para los tres mensajes, y la lista es el último return. La comprobación busca en cada sección tanto los mensajes como la lista. Si la lista no tiene condición, también está en la sección «Cargando», aunque esté vacía: un <ul> sin filas sigue siendo una lista para un lector de pantalla.Pruébalo
Completa el hueco
«Cargando…» debe aparecer solo mientras status es loading.
{status === 'loading' <p role="status">Cargando…</p>}&&: si el lado izquierdo es falso, la expresión es false y React no dibuja nada. Si es verdadero, la expresión es el elemento de la derecha.
La idea
Agotado: una insignia y un botón desactivado
Una condición no es solo para partes enteras. En la tarjeta del producto cambia un texto y un atributo:
{product.stock === 0 ? ( <p className="badge">Agotado</p>) : ( <p>Quedan {product.stock} uds.</p>)}<button type="button" disabled={product.stock === 0}>Añadir</button>disabled={true} pone el atributo disabled, y disabled={false} lo quita.
Un botón desactivado no se puede pulsar, no recibe el foco con Tab, y un lector de pantalla dice que no está disponible. La insignia dice por qué. Un botón que se puede pulsar y luego dice «sin existencias» es peor: el comprador se entera solo después del clic.
Pruébalo
Agotado
Haz que ProductCard muestre «Agotado» y desactive el botón «Añadir» cuando stock sea 0. Si no, muestra «Quedan 5 uds.» y el botón funciona. El queso está agotado.
Consola
Solución
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
{product.stock === 0 ? (
<p className="badge">Agotado</p>
) : (
<p>Quedan {product.stock} uds.</p>
)}
<button type="button" aria-label={`Añadir ${product.name}`} disabled={product.stock === 0}>Añadir</button>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const PRODUCTS = [
{ id: 1, name: 'Pan blanco', price: 1.2, stock: 5 },
{ id: 3, name: 'Queso blanco', price: 7.9, stock: 0 },
{ id: 2, name: 'Leche fresca', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<h1>El catálogo</h1>
<ul>
{PRODUCTS.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
? : elige entre la insignia y «Quedan … uds.», y disabled={product.stock === 0} desactiva el botón. La comprobación busca además un 0 suelto en la página: {product.stock && <p>…</p>} dibuja un 0 para el queso. De eso trata la tarjeta siguiente.La idea
El cero en la pantalla
&& no devuelve true o false. Devuelve el lado izquierdo si es falsy, y el lado derecho si es truthy. React dibuja los números, el 0 incluido, y no dibuja false, null ni undefined. Así que:
{items.length && <p>En el carrito: {items.length}</p>} // carrito vacío: 0{items.length > 0 && <p>En el carrito: {items.length}</p>} // carrito vacío: nadaAntes de && pon una comparación que dé true o false: > 0, === 0, !== null. Otra manera es ? : con null para «nada».
Pruébalo
Encuentra el error
Con el carrito vacío, en la esquina de arriba pone «0».
Elige la línea con el error.
Línea 4: items.length && … es 0 con el carrito vacío, y React dibuja el 0. Tiene que ser items.length > 0 && ….
La idea
Quitado u oculto
A veces un elemento tiene que quedarse en el DOM y solo no verse. Aquí los dos paneles tienen un campo para una nota. Abre los dos, escribe algo, ciérralos y ábrelos otra vez.
Consola
{removedOpen && …}: cerrado desaparece, y abierto es nuevo, con el campo vacío. En la segunda usa el atributo hidden={!hiddenOpen}: se queda en el DOM con display: none y conserva lo escrito.Quitar es lo habitual: menos elementos en la página, nada oculto para un lector de pantalla. Ocultar es para partes que se abren y se cierran a menudo o tienen que conservar lo que se introdujo en ellas.
Más precisamente, para curiosos
hidden y CSS. hidden funciona mediante display: none de los estilos del navegador. Si una regla de CSS da al elemento display: flex, gana la regla y el elemento sigue visible. Entonces lo ocultas con una clase o con style={{ display: 'none' }}.
Mismo lugar, mismo elemento. Con {isSearch ? <input placeholder="Buscar" /> : <input placeholder="Código" />} cada rama es un <input> en el mismo lugar. React conserva el elemento y solo cambia placeholder, y lo escrito se queda. Si necesitas dos elementos distintos, dales key distintas.
return null. Un componente que no debe dibujar nada devuelve null.
Los componentes de clase del código antiguo hacen lo mismo en el método render().
aria-hidden no es ocultar. aria-hidden="true" oculta el elemento solo de un lector de pantalla, y se queda en la pantalla. Ocultar para todos es display: none: hidden, v-show o quitar.
Ejercicio
El carrito
App muestra tres carritos: vacío, con productos y con un producto agotado. Haz Cart de modo que el vacío diga «El carrito está vacío.», sin lista y sin total, y el botón «Hacer el pedido» esté desactivado. El lleno muestra las filas, «Total: 4,00 €» y un botón que funciona. El producto agotado tiene una insignia «Agotado», encima del botón dice «Quita los productos agotados para hacer el pedido.» (rol alert), y el botón está desactivado.
Consola
Solución
Cart.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function Cart({ items }) {
const total = items.reduce((sum, p) => sum + p.price * p.qty, 0);
const soldOut = items.some((p) => p.stock === 0);
return (
<div>
{items.length === 0 ? (
<p>El carrito está vacío.</p>
) : (
<>
<ul>
{items.map((p) => (
<li key={p.id}>
{p.name} × {p.qty}
{p.stock === 0 && <span className="badge">Agotado</span>}
</li>
))}
</ul>
<p>Total: {money(total)}</p>
</>
)}
{soldOut && <p role="alert">Quita los productos agotados para hacer el pedido.</p>}
<button type="button" disabled={items.length === 0 || soldOut}>Hacer el pedido</button>
</div>
);
}
App.jsximport Cart from './Cart';
export default function App() {
return (
<main>
<section aria-label="Vacío">
<h2>Vacío</h2>
<Cart items={[]} />
</section>
<section aria-label="Con productos">
<h2>Con productos</h2>
<Cart items={[
{ id: 1, name: 'Pan blanco', price: 1.2, qty: 2, stock: 5 },
{ id: 2, name: 'Leche fresca', price: 1.6, qty: 1, stock: 2 },
]} />
</section>
<section aria-label="Con un producto agotado">
<h2>Con un producto agotado</h2>
<Cart items={[
{ id: 1, name: 'Pan blanco', price: 1.2, qty: 1, stock: 5 },
{ id: 3, name: 'Queso blanco', price: 7.9, qty: 1, stock: 0 },
]} />
</section>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
items.length === 0 ? … : … elige entre el mensaje y la lista con el total (los dos van en <>…</>). soldOut && <p role="alert">…</p> muestra el aviso, y disabled={items.length === 0 || soldOut} desactiva el botón en ambos casos. Con items.length && … el carrito vacío muestra un 0, y la comprobación lo busca.Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Datos desde una API · El primer componente · Props: datos desde arriba · Listas y claves
- Continúa en: Actualizar objetos y arrays · Estado derivado · Estado compartido: hacia arriba · Efectos
- Ejercicios sobre el tema: Componentes