En esta página

Props y Children

14 min lectura TextoCap. 1 — Fundamentos de React

¿Qué son las props?

Las props (properties) son el mecanismo de React para pasar datos de un componente padre a un componente hijo. Son análogas a los parámetros de una función: el padre decide qué valores pasa, y el hijo los recibe como un objeto de solo lectura.

// El padre controla los valores
function App() {
  return <Saludo nombre="María" formal={true} />;
}

// El hijo recibe y usa las props
function Saludo({ nombre, formal }: { nombre: string; formal: boolean }) {
  const prefijo = formal ? 'Buenos días,' : '¡Hola,';
  return <h1>{prefijo} {nombre}!</h1>;
}

Las props son inmutables desde la perspectiva del hijo: un componente nunca debe modificar sus propias props. Si necesita cambiar un valor, eso es responsabilidad del estado (que veremos en la próxima lección).

Tipado de props con interfaces TypeScript

La forma correcta y escalable de tipar props es mediante interfaces:

interface PerfilProps {
  // Props requeridas (sin ?)
  nombre: string;
  email: string;
  avatarUrl: string;
  // Props opcionales (con ?)
  biografia?: string;
  seguidores?: number;
  verificado?: boolean;
}

function Perfil({
  nombre,
  email,
  avatarUrl,
  biografia = 'Sin biografía',
  seguidores = 0,
  verificado = false,
}: PerfilProps): React.JSX.Element {
  return (
    <div className="perfil">
      <img src={avatarUrl} alt={`Foto de perfil de ${nombre}`} />
      <h2>
        {nombre}
        {verificado && <span aria-label="verificado">✓</span>}
      </h2>
      <p>{email}</p>
      <p>{biografia}</p>
      <p>{seguidores.toLocaleString('es-ES')} seguidores</p>
    </div>
  );
}

Destructuring de props

Hay dos formas de acceder a las props:

// Forma 1: object parameter
function Componente(props: MisProps) {
  return <p>{props.nombre}</p>;
}

// Forma 2: destructuring directo (recomendada)
function Componente({ nombre, edad }: MisProps) {
  return <p>{nombre}, {edad} años</p>;
}

// Destructuring con renombre de variable
function Componente({ nombre: nombreUsuario }: MisProps) {
  return <p>{nombreUsuario}</p>;
}

El destructuring en la firma de la función es la forma más idiomática en código React moderno porque hace explícitas las dependencias del componente.

Props de función y callbacks

Las props pueden ser funciones, lo que permite que los hijos comuniquen eventos a los padres:

interface FilaTablaProps {
  item: { id: string; nombre: string; precio: number };
  onEditar: (id: string) => void;
  onEliminar: (id: string) => void;
  onSeleccionar: (id: string, seleccionado: boolean) => void;
}

function FilaTabla({ item, onEditar, onEliminar, onSeleccionar }: FilaTablaProps) {
  return (
    <tr>
      <td>{item.nombre}</td>
      <td>${item.precio}</td>
      <td>
        <button type="button" onClick={() => onEditar(item.id)}>Editar</button>
        <button type="button" onClick={() => onEliminar(item.id)}>Eliminar</button>
        <input
          type="checkbox"
          onChange={(e) => onSeleccionar(item.id, e.target.checked)}
        />
      </td>
    </tr>
  );
}

El prop `children`

children es la prop especial que representa el contenido que se coloca entre las etiquetas de apertura y cierre de un componente. Permite la composición: el padre decide qué renderizar dentro del componente.

interface ContenedorProps {
  children: React.ReactNode;
  className?: string;
}

function Contenedor({ children, className = '' }: ContenedorProps) {
  return (
    <div className={`contenedor ${className}`}>
      {children}
    </div>
  );
}

// Uso
<Contenedor className="destacado">
  <h2>Título</h2>
  <p>Cualquier contenido puede ir aquí</p>
  <Boton etiqueta="Acción" />
</Contenedor>

React.ReactNode es el tipo más flexible para children: acepta elementos JSX, strings, números, null, undefined, arrays y fragmentos. Es el tipo correcto en casi todos los casos.

Patrones de composición avanzados

Componentes compuestos (Compound Components)

Permiten una API más expresiva para componentes complejos:

interface AccordionProps {
  children: React.ReactNode;
}

interface AccordionItemProps {
  titulo: string;
  children: React.ReactNode;
}

function Accordion({ children }: AccordionProps) {
  return <div className="accordion">{children}</div>;
}

function AccordionItem({ titulo, children }: AccordionItemProps) {
  return (
    <details className="accordion-item">
      <summary>{titulo}</summary>
      <div className="accordion-contenido">{children}</div>
    </details>
  );
}

// Uso natural y expresivo
<Accordion>
  <AccordionItem titulo="¿Cómo funciona?">
    <p>Explicación detallada aquí…</p>
  </AccordionItem>
  <AccordionItem titulo="¿Cuánto cuesta?">
    <p>Los precios varían según el plan.</p>
  </AccordionItem>
</Accordion>

Render Props

Permite que un componente delegue cómo renderizar al padre:

interface ListaConRenderProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function ListaConRender<T>({ items, renderItem, keyExtractor }: ListaConRenderProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// Uso con tipo inferido
<ListaConRender
  items={usuarios}
  keyExtractor={(u) => u.id}
  renderItem={(usuario) => (
    <span>{usuario.nombre} — {usuario.rol}</span>
  )}
/>

Extender elementos HTML nativos

Un patrón muy útil es crear componentes que extiendan elementos HTML nativos:

// Extiende todos los atributos de un <button> + los nuestros
interface BotonPrimarioProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  cargando?: boolean;
}

function BotonPrimario({ cargando = false, children, disabled, ...resto }: BotonPrimarioProps) {
  return (
    <button
      type="button"
      className="btn-primario"
      disabled={disabled || cargando}
      aria-busy={cargando}
      {...resto}
    >
      {cargando ? 'Cargando…' : children}
    </button>
  );
}

// Acepta onClick, aria-label, id, y cualquier atributo de button
<BotonPrimario onClick={enviar} aria-label="Guardar cambios" cargando={guardando}>
  Guardar
</BotonPrimario>

Prop drilling y sus límites

Cuando los datos necesitan pasar por múltiples niveles de componentes, el prop drilling se vuelve problemático:

App → Layout → Pagina → Seccion → Tarjeta → Avatar (usa el usuario)

Si solo Avatar necesita el usuario, pasar la prop por todos los niveles intermedios es tedioso y frágil. La solución es el Context API (que veremos en la lección 8) o un gestor de estado global.

Regla práctica: si una prop pasa por más de 2-3 niveles sin ser usada, es momento de considerar Context o estado global.

En la próxima lección aprenderemos useState para agregar estado interno a los componentes y manejar eventos de usuario.

Prefiere interfaces sobre type para props de componentes
Las interfaces de TypeScript son extensibles y generan mejores mensajes de error en componentes React. Usa type solo cuando necesites uniones, intersecciones o tipos utilitarios. Para props simples de componentes, interface es siempre la mejor opción.
Spread de props — úsalo con cuidado
El patrón {...props} puede simplificar wrappers pero hace difícil rastrear qué props acepta un componente. Si lo usas, define explícitamente qué props interceptas y cuáles se pasan al elemento nativo con Omit<> o Pick<>.
React.ReactNode vs React.JSX.Element
React.ReactNode es el tipo más amplio para children: acepta JSX, strings, numbers, null, undefined, arrays y fragments. React.JSX.Element solo acepta elementos JSX. Para children usa siempre React.ReactNode a menos que necesites restricción estricta.