learning-front

Nivel 6 · React de cero a héroe (con TypeScript)

Props y composición

Pasar datos a un componente con props (tipadas), desestructurarlas, usar children para componentes envoltorio y entender por qué el flujo de datos va siempre de padres a hijos.

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.

tsx
// 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.

tsx
// 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.

tsx
// 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.

tsx
// 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.

tsx
// 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
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.