Props: datos desde arriba
En resumen: Los props son los datos que un componente recibe de su padre, igual que una función recibe parámetros; el hijo no los cambia, y children trae contenido desde fuera.
Problema
Tres veces pan
El catálogo tiene que mostrar tres productos: pan, leche y queso. La tarjeta de producto está lista de la lección anterior, así que App la pone tres veces.
Consola
Las tres tarjetas son pan. El producto está escrito dentro de ProductCard, y todas las tarjetas lo muestran. El otro camino sería BreadCard, MilkCard y CheeseCard: tres copias del mismo componente, y cada cambio en la tarjeta habría que hacerlo tres veces. Lo que falta es una forma de decirle a la tarjeta qué producto mostrar.
La idea
Props: datos desde arriba
Un componente es una función. Una función recibe sus datos como parámetros, y un componente los recibe como atributos de su padre: el componente que lo usa. Estos atributos se llaman props (de «properties»). El padre los escribe, el hijo los lee.
<ProductCard product={bread} /><ProductCard product={milk} />export default function ProductCard({ product }) { return <h2>{product.name}</h2>;}React reúne todos los atributos en un objeto y se lo pasa a la función: { product: bread }. La notación ({ product }) saca de él la propiedad product (desestructuración). Sin las llaves el parámetro es el objeto entero: function ProductCard(props), y luego props.product.name.
Hay una sola tarjeta, y cada uso tiene datos distintos. Con una función pasa lo mismo: una definición, muchas llamadas con argumentos distintos.
Míralo
Mira cómo fluyen
const bread = { name: 'Pan blanco', price: 1.2 };const milk = { name: 'Leche fresca', price: 1.6 };const cheese = { name: 'Queso blanco', price: 7.9 };
function ProductCard({ product, discount = 0 }) { return ( <article> <h2>{product.name}</h2> <p>Precio: {money(product.price * (1 - discount / 100))}</p> </article> );}
export default function App() { return ( <main> <h1>Pan y leche</h1> <div> <ProductCard product={bread} /> <ProductCard product={milk} /> <ProductCard product={cheese} discount={10} /> </div> </main> );}Paso a paso, en palabras
- App guarda los datos: tres productos. Debajo hay tres ProductCard: el mismo componente tres veces, y ninguna sabe aún qué mostrar.
- product={bread} le pasa el objeto bread a la primera tarjeta. Los atributos de un componente se llaman props.
- Las otras dos tarjetas reciben su propio producto. La tercera recibe además discount = 10. Cada tarjeta tiene sus propios props.
- Cada tarjeta se dibuja a partir de sus props. El pan y la leche no recibieron discount, por eso vale 0 por defecto. El queso lleva un 10 % de descuento: 7,11 €.
- Si la tarjeta intenta props.discount = 0, no sube nada: en modo de desarrollo React lanza un TypeError (el objeto de props está congelado), y los datos de App siguen igual.
- Los datos fluyen en una sola dirección: del padre hacia abajo, al hijo. Para cambiar algo arriba, el hijo tiene que pedírselo al padre. Ese es el módulo «Estado».
Cada tarjeta conoce solo sus propios props. No ve las otras tarjetas, y no sabe de dónde salen los datos de App.
Míralo
Una tarjeta, tres productos
Consola
Los productos están en App, y hay una sola ProductCard. Añade un cuarto producto en App (la imagen img/apples.svg existe) y un <ProductCard … /> más. La tarjeta no la tocas. Con 30 productos querrás una lista en lugar de 30 líneas: es la próxima lección.
Pruébalo
Encuentra el error
La tarjeta no se muestra, y en la consola dice «TypeError: Cannot read properties of undefined (reading 'toFixed')».
Elige la línea con el error.
product="bread" pasa el texto «bread», no el objeto. El texto no tiene price, así que product.price es undefined y money falla en toFixed. El objeto se pasa entre llaves: product={bread}.La idea
Un valor por defecto
Algunos productos están en oferta. La tarjeta recibe un segundo prop, discount, en porcentaje. La mayoría de los productos no tienen descuento, y no queda bien que cada línea de App escriba discount={0}. Un prop que falta es undefined, igual que un argumento que falta en una función. El precio se convierte en price * (1 - undefined / 100), o sea NaN.
Por eso el prop recibe un valor por defecto:
export default function ProductCard({ product, discount = 0 }) {Es un valor por defecto de la desestructuración de JavaScript: si falta discount, vale 0. En código antiguo verás ProductCard.defaultProps = { discount: 0 } y propTypes. React 19 ya no los mira en los componentes de función: comprobar los tipos es trabajo de TypeScript.
Pruébalo
Un descuento
App le pasa al queso un discount de 10 (por ciento). Haz que la tarjeta acepte discount, 0 por defecto, y que muestre el precio con el descuento: el pan 1,20 €, la leche 1,60 €, el queso 7,11 €.
Consola
Solución
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product, discount = 0 }) {
return (
<article>
<h2>{product.name}</h2>
<p>Precio: {money(product.price * (1 - discount / 100))}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const bread = { name: 'Pan blanco', price: 1.2 };
const milk = { name: 'Leche fresca', price: 1.6 };
const cheese = { name: 'Queso blanco', price: 7.9 };
export default function App() {
return (
<main>
<h1>Pan y leche</h1>
<div>
<ProductCard product={bread} />
<ProductCard product={milk} />
<ProductCard product={cheese} discount={10} />
</div>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
function ProductCard({ product, discount = 0 }) y el precio money(product.price * (1 - discount / 100)). Sin = 0 el pan y la leche muestran «NaN €»: la comprobación mira los tres precios.
La idea
Solo lectura
Los datos son de quien los tiene. La tarjeta puede leer sus props, pero no puede cambiarlos. Prueba el botón «Uno más»: el hijo intenta aumentar su propio qty.
Consola
La cantidad se queda en 2, y la consola dice «TypeError: Cannot assign to read only property 'qty' of object». En modo de desarrollo React congela el objeto de props, justo para atrapar código como este.
Aunque pasara, el cambio sería solo en el hijo: App seguiría diciendo 2, y en el siguiente dibujo App volvería a pasar 2. Dos lugares con verdades distintas son el error de «Una lista a partir de datos». La regla: los datos fluyen hacia abajo, y cambiarlos es trabajo del padre. Cómo le pide un hijo a su padre que cambie algo es el módulo «Estado».
La idea
Contenido desde fuera
La tienda tiene marcos que se ven igual y llevan cosas distintas dentro: «Envío» con dos párrafos, «Devoluciones» con uno. El título es un prop. El contenido es un trozo entero de HTML, y es más cómodo escribirlo entre la etiqueta de apertura y la de cierre, como con <section>.
<Card title="Envío"> <p>En un máximo de 2 días laborables.</p></Card>export default function Card({ title, children }) { return ( <section className="card"> <h2>{title}</h2> {children} </section> );}Todo lo que hay entre <Card> y </Card> llega en el prop especial children. Card decide dónde lo pone.
Consola
Pruébalo
El marco
Card muestra solo el título. Haz que muestre también, bajo el título, el contenido que App escribe entre <Card> y </Card>.
Consola
Solución
Card.jsxexport default function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
App.jsximport Card from './Card';
export default function App() {
return (
<main>
<h1>El carrito</h1>
<Card title="Envío">
<p>En un máximo de 2 días laborables.</p>
<p>Gratis a partir de 30 €.</p>
</Card>
<Card title="Devoluciones">
<p>Hasta 14 días desde la entrega.</p>
</Card>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
Card también recibe children: function Card({ title, children }), y lo pone bajo el título: {children}. Sin las llaves la página muestra la palabra «children». Si lo pones dentro de <h2>, los párrafos pasan a ser parte del título, y ya no es «Envío».
Más precisamente, para curiosos
Más de un lugar para contenido. Hay un solo children. Si el marco también tiene una parte de abajo, es un prop más con JSX: <Card title="Envío" footer={<a href="/delivery">Detalles</a>}>…</Card>, y Card pone {footer} donde corresponde.
Todas las propiedades de golpe. <ProductCard {...product} /> pasa cada propiedad del objeto como un prop aparte: name, price, image. Es cómodo, pero no se ve qué recibe exactamente la tarjeta.
Solo el primer nivel está congelado. props.qty = 3 lanza un error, pero props.product.price = 2 pasa en silencio: el objeto del producto no está congelado, es el mismo objeto que en App. Así el hijo cambia los datos del padre a sus espaldas. No lo hagas.
Tipos. React 19 ya no comprueba propTypes en los componentes de función y no usa defaultProps en ellos. Los tipos de los props se escriben con TypeScript: aparece en la lección sobre un proyecto real.
Ejercicio
El escaparate
Construye el escaparate desde cero. ProductCard recibe product y discount (0 por defecto) y muestra la imagen (alt es el nombre), el nombre como título de nivel dos y «Precio: …» con el descuento. Card recibe title y muestra el título (nivel dos) y el contenido desde fuera. En App, bajo el título: las tres tarjetas en un div (el queso con discount 10), después una Card «Envío» con «En un máximo de 2 días laborables.» y «Gratis a partir de 30 €.».
Consola
Solución
App.jsximport ProductCard from './ProductCard';
import Card from './Card';
const bread = { name: 'Pan blanco', price: 1.2, image: 'img/bread.svg' };
const milk = { name: 'Leche fresca', price: 1.6, image: 'img/milk.svg' };
const cheese = { name: 'Queso blanco', price: 7.9, image: 'img/cheese.svg' };
export default function App() {
return (
<main>
<h1>El escaparate</h1>
<div>
<ProductCard product={bread} />
<ProductCard product={milk} />
<ProductCard product={cheese} discount={10} />
</div>
<Card title="Envío">
<p>En un máximo de 2 días laborables.</p>
<p>Gratis a partir de 30 €.</p>
</Card>
</main>
);
}
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product, discount = 0 }) {
return (
<article>
<img src={product.image} alt={product.name} />
<h2>{product.name}</h2>
<p>Precio: {money(product.price * (1 - discount / 100))}</p>
</article>
);
}
Card.jsxexport default function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
ProductCard: function ProductCard({ product, discount = 0 }), <img src={product.image} alt={product.name} />, <h2>{product.name}</h2> y el precio con el descuento. Card: function Card({ title, children }) con <section className="card">, <h2>{title}</h2> y {children}. App: <ProductCard product={cheese} discount={10} /> y el contenido entre <Card title="Envío"> y </Card>. La comprobación encuentra las imágenes por alt, los precios de las tres tarjetas y los párrafos dentro de .card.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Texto, enlaces, imágenes, tablas · Accesibilidad · Una lista a partir de datos · El primer componente
- Continúa en: Listas y claves · Condiciones: vacío, error, cargando · Estado · Estado derivado
- Ejercicios sobre el tema: Componentes