En el capítulo anterior compusiste la interfaz con componentes, pero cada uno llevaba sus datos
clavados dentro: un HeroCard que siempre dice “Tracer” no sirve para un equipo de treinta
héroes. La pieza que falta es poder pasarle datos desde fuera. Eso son las props, y son lo
que convierte un componente en una plantilla reutilizable.
Pasar datos con props#
A un componente se le pasan datos como si fueran atributos de una etiqueta. Esos datos se llaman props. El componente los recibe todos juntos, en un objeto, como primer (y único) parámetro de su función.
// HeroCard recibe un objeto con sus props como parámetro (lo tipamos en línea).
// Leemos props.nombre y props.rol.
function HeroCard(props: { nombre: string; rol: string }) {
return (
<article className="tarjeta">
<h2>{props.nombre}</h2>
<p>{props.rol}</p>
</article>
);
}
// Al usar el componente, las props se pasan como atributos.
const tarjeta = <HeroCard nombre="Tracer" rol="Daño" />;El mismo HeroCard con nombre="Mercy" rol="Apoyo" pintaría otra tarjeta distinta. Esa es toda
la idea: un componente, muchos datos.
Desestructurar las props#
Escribir props.nombre, props.rol… una y otra vez es ruidoso. Como las props llegan en un
objeto, puedes desestructurarlas en el propio parámetro, justo como aprendiste en el Nivel 3.
// Desestructuramos el objeto de props directamente en el parámetro:
// en vez de (props), escribimos ({ nombre, rol }) y ya tenemos cada valor suelto.
function HeroCard({ nombre, rol }: { nombre: string; rol: string }) {
return (
<article className="tarjeta">
<h2>{nombre}</h2>
<p>{rol}</p>
</article>
);
}Tipar las props#
Escribir el tipo en línea está bien para dos campos, pero en cuanto crece conviene darle un
nombre: extraes la forma a una interface (o un type) y se la pones al parámetro. Es más
legible y reutilizable. Y, como antes, si alguien usa el componente y se olvida de un dato o le
pasa un número donde iba un texto, el error salta antes de ejecutar.
// La forma de las props que HeroCard espera.
interface HeroCardProps {
nombre: string;
rol: string;
}
// Le ponemos el tipo al parámetro desestructurado.
function HeroCard({ nombre, rol }: HeroCardProps) {
return (
<article className="tarjeta">
<h2>{nombre}</h2>
<p>{rol}</p>
</article>
);
}
// Si te olvidas de "rol", TypeScript lo marca aquí, no en ejecución.
const tarjeta = <HeroCard nombre="Tracer" rol="Daño" />;Un objeto entero como prop#
Cuando un componente necesita muchos datos relacionados (nombre, rol, partidas, victorias…), pasar un prop por cada uno se vuelve incómodo. Suele ser mejor pasar un solo prop con el objeto entero, reutilizando el tipo que ya tienes.
// El tipo Heroe, el mismo del Nivel 5.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// HeroCard recibe UN prop, hero, con todo el héroe dentro.
function HeroCard({ hero }: { hero: Heroe }) {
// Con los datos del héroe, calculamos el winrate (dato derivado).
const winrate = (hero.victorias / hero.partidas) * 100;
return (
<article className="tarjeta">
<h2>{hero.nombre}</h2>
<p>{hero.rol}</p>
{/* toFixed(0) redondea; el "%" es texto normal */}
<p>{winrate.toFixed(0)}% de victorias</p>
</article>
);
}
// Se pasa el objeto con llaves: hero={tracer}.
const tracer: Heroe = { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 };
const tarjeta = <HeroCard hero={tracer} />;children: el contenido entre las etiquetas#
Hay una prop especial, children, que recibe lo que escribes entre la etiqueta de
apertura y la de cierre de un componente. Sirve para hacer componentes envoltorio: una caja
reutilizable que no sabe de antemano qué llevará dentro.
// ReactNode es el tipo de "cualquier cosa que se pueda pintar" (texto, elementos...).
import type { ReactNode } from "react";
// Card solo sabe de la caja; su contenido llega por la prop children.
function Card({ children }: { children: ReactNode }) {
return <article className="tarjeta">{children}</article>;
}
// Lo que va entre <Card> y </Card> es children.
const tarjeta = (
<Card>
<h2>Tracer</h2>
<p>Daño</p>
</Card>
);Así puedes construir HeroCard sobre Card: Card pone el marco, HeroCard pone los datos
dentro. Eso es composición llevada un paso más allá.
Los datos van en una sola dirección#
Una regla que conviene grabar desde ya: en React los datos fluyen de padres a hijos, a través de las props, y las props son de solo lectura. Un componente usa lo que recibe, pero no lo cambia.
¿Y qué pasa si lo intentas? Imagina que dentro de HeroCard escribes hero.nombre = "Otro".
Pasan dos cosas, las dos malas. Una: React no se entera de ese cambio (las props no son el canal
por el que detecta que algo ha cambiado —eso es el estado, que verás pronto—), así que la
pantalla no se actualiza y te vuelves loco buscando por qué. Dos, y peor: ese hero es el
mismo objeto que tiene el padre —recuerda del Nivel 3 que los objetos se pasan por
referencia—, así que al mutarlo le cambias el dato al padre a su espalda, y el fallo acaba
apareciendo en otro sitio, lejos de donde lo causaste. Otra vez el patrón de los bugs malos: no
crashea, miente en silencio.
Por eso la regla: si algo tiene que cambiar, se cambia arriba, en quien es dueño del dato, y vuelve a bajar ya cambiado. A esto se le llama flujo unidireccional, y es lo que hace que, mirando un componente, sepas siempre de dónde viene cada dato y que nadie te lo va a cambiar por detrás. En el próximo capítulo verás cómo hacer que esos datos cambien con el tiempo —el estado—, pero la dirección no cambia: siempre hacia abajo.
Pruébalo#
La misma tarjeta, ahora alimentada por props: App tiene los datos y se los pasa a cada
HeroCard. Cambia un héroe, añade otro <HeroCard hero={...} />, y pulsa Ejecutar.
Comprueba lo que sabes#
Pregunta 1 de 4
¿Qué son las props de un componente?
Tu turno#
Ejercicio · en esta página
Un equipo de héroes con props
Haz que HeroCard reciba los datos del héroe por props (tipadas) y renderiza varios héroes distintos con el mismo componente desde App.
Paso 1: Props tipadas y reutilización
- HeroCard recibe props individuales tipadas con una interface (nombre, rol)
- Desestructura las props en el parámetro
- App renderiza dos héroes distintos con el mismo HeroCard
Paso 2: Un objeto como prop
- HeroCard recibe un solo prop: el héroe entero (objeto tipado con interface Heroe)
- El winrate se calcula dentro del componente
- Los datos viven en App y bajan por props
Paso 3: Composición con children
- Un componente envoltorio Card que usa children, y HeroCard construido sobre él
- Tres héroes, con el flujo de datos de App hacia abajo
- Aguanta a 375px sin romperse
Ver soluciones
// La forma de las props que HeroCard espera: dos textos.
interface HeroCardProps {
nombre: string;
rol: string;
}
// HeroCard recibe sus datos por props y los desestructura en el parámetro.
// Las props son de SOLO LECTURA: el componente las usa, no las cambia.
function HeroCard({ nombre, rol }: HeroCardProps) {
return (
<article className="tarjeta">
<h2 className="tarjeta__nombre">{nombre}</h2>
<p className="tarjeta__rol">{rol}</p>
</article>
);
}
// App pasa datos DISTINTOS a cada HeroCard: el mismo componente, varios héroes.
export default function App() {
return (
<section className="equipo">
<HeroCard nombre="Tracer" rol="Daño" />
<HeroCard nombre="Mercy" rol="Apoyo" />
</section>
);
} Por qué este nivel
- El salto del capítulo: la tarjeta deja de tener los datos clavados. Recibe nombre y rol por props, así que el MISMO HeroCard sirve para Tracer, para Mercy y para quien sea. Tipar las props con una interface hace que pasarle algo que no toca dé error antes de ejecutar.
// La forma de un héroe, como la definiste en el Nivel 5.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// HeroCard recibe UN solo prop: el héroe entero. Mejor que muchos props sueltos.
function HeroCard({ hero }: { hero: Heroe }) {
// Dato derivado: el winrate se calcula a partir del héroe.
const winrate = (hero.victorias / hero.partidas) * 100;
return (
<article className="tarjeta">
<h2 className="tarjeta__nombre">{hero.nombre}</h2>
<p className="tarjeta__rol">{hero.rol}</p>
<p className="tarjeta__winrate">{winrate.toFixed(0)}% de victorias</p>
</article>
);
}
// App es el dueño de los datos. Bajan a cada tarjeta por props (flujo unidireccional).
export default function App() {
const tracer: Heroe = { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 };
const mercy: Heroe = { nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 150 };
return (
<section className="equipo">
<HeroCard hero={tracer} />
<HeroCard hero={mercy} />
</section>
);
} Por qué es mejor que el anterior
- En vez de un prop por cada campo (nombre, rol, partidas, victorias...), pasa UN solo prop: el héroe entero. Menos ruido, y el tipo Heroe es la misma fuente de verdad que ya tenías en el Nivel 5.
- Los datos viven en App y bajan a las tarjetas: ese es el flujo unidireccional, el que hace que sepas siempre de dónde viene cada dato.
// ReactNode es el tipo de "cualquier cosa pintable" (texto, elementos, varios...).
import type { ReactNode } from "react";
// La forma de un héroe, como en el Nivel 5.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// Card: un envoltorio reutilizable. Recibe por la prop especial children
// el contenido que se le mete entre <Card> y </Card>.
function Card({ children }: { children: ReactNode }) {
return <article className="tarjeta">{children}</article>;
}
// HeroCard se construye SOBRE Card (composición): le mete dentro los datos del héroe.
function HeroCard({ hero }: { hero: Heroe }) {
// El winrate es un dato derivado del héroe.
const winrate = (hero.victorias / hero.partidas) * 100;
return (
<Card>
<h2 className="tarjeta__nombre">{hero.nombre}</h2>
<p className="tarjeta__rol">{hero.rol}</p>
<p className="tarjeta__winrate">{winrate.toFixed(0)}% de victorias</p>
</Card>
);
}
// App es el dueño de los datos; bajan a cada tarjeta por props (flujo unidireccional).
export default function App() {
const tracer: Heroe = { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 };
const mercy: Heroe = { nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 150 };
const reinhardt: Heroe = { nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 52 };
return (
<section className="equipo">
<HeroCard hero={tracer} />
<HeroCard hero={mercy} />
<HeroCard hero={reinhardt} />
</section>
);
} Por qué es mejor que el anterior
- Aparece la composición de verdad: un Card genérico que solo sabe de la caja (el marco, la sombra) y recibe su contenido por children. HeroCard se construye SOBRE Card y pone dentro los datos. Card se podría reutilizar para cualquier otra tarjeta del proyecto.
- Tres héroes escritos a mano dejan ver el límite: para una lista de verdad, repetir <HeroCard /> no escala. Eso se resuelve en el próximo capítulo.