Props en React: entradas, contratos y flujo de datos unidireccional

Capítulo 3

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Props: entradas inmutables y flujo de datos unidireccional

En React, las props son los datos que un componente recibe desde su padre. Funcionan como entradas (inputs) y, por diseño, se tratan como inmutables dentro del componente que las recibe: un componente no “cambia” sus props; si necesita un valor distinto, debe pedirlo indirectamente (por ejemplo, notificando al padre mediante callbacks) para que el padre renderice con nuevas props.

Este modelo encaja con el flujo de datos unidireccional: los datos bajan (padre → hijo) y los eventos suben (hijo → padre) mediante funciones. Esto hace que el comportamiento sea más predecible: si algo cambia en la UI, puedes rastrear de dónde viene mirando el árbol de componentes hacia arriba.

Props como contrato entre componentes

Piensa en cada componente como una función con firma: UI = f(props). Ese “contrato” debería dejar claro:

  • Qué recibe: datos y opciones necesarias para renderizar.
  • Qué muestra: el resultado visual esperado para esas entradas.
  • Qué delega: acciones que no resuelve internamente y expone como callbacks (por ejemplo, onSelect, onChange, onSubmit).

Un buen contrato de props reduce acoplamiento: el componente no necesita saber de dónde vienen los datos ni cómo se guardan; solo sabe renderizar y emitir eventos.

Patrones esenciales de props

1) Props simples (datos directos)

Son valores primitivos o estructuras simples que el componente usa para renderizar.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

function UserBadge({ name, isOnline }) {  return (    <div>      <strong>{name}</strong>      <span>{isOnline ? "En línea" : "Desconectado"}</span>    </div>  );}

Contrato: recibe name e isOnline; muestra un badge; no delega acciones.

2) Props de configuración (variantes y opciones)

Cuando un componente tiene variantes (tamaño, tono, estado), es común usar props de configuración. La clave es que sean predecibles y no se conviertan en una combinación explosiva de flags.

function Button({ variant = "primary", size = "md", disabled = false, onClick, children }) {  const className = `btn btn--${variant} btn--${size}`;  return (    <button className={className} disabled={disabled} onClick={onClick}>      {children}    </button>  );}

Recomendación: si hay muchas opciones relacionadas, considera agruparlas (ver sección de legibilidad) o limitar el set de variantes soportadas.

3) children: composición en lugar de configuración excesiva

children permite que el padre “inyecte” contenido dentro del componente. Es ideal cuando el componente define un contenedor/estructura, pero el contenido interno debe ser flexible.

function Card({ title, children, footer }) {  return (    <section className="card">      <header className="card__header">        <h4>{title}</h4>      </header>      <div className="card__body">{children}</div>      {footer ? <div className="card__footer">{footer}</div> : null}    </section>  );}
<Card title="Perfil" footer={<button>Guardar</button>}>  <p>Contenido libre dentro de la tarjeta.</p></Card>

Nota: además de children, a veces se usa un prop como footer o header para “slots” específicos cuando quieres controlar zonas concretas sin forzar al usuario a recrear toda la estructura.

4) Props por defecto (defaults)

Los valores por defecto hacen el componente más fácil de usar y reducen ruido en el padre. En componentes funcionales, se suelen definir en la desestructuración.

function Avatar({ src, alt = "Avatar", size = 40 }) {  return <img src={src} alt={alt} width={size} height={size} />;}

Regla práctica: define defaults para opciones, no para datos obligatorios. Si src es imprescindible, no lo “maquilles” con un default silencioso; mejor fallar de forma visible durante desarrollo.

Guía práctica: diseñar el contrato de props paso a paso

Paso 1: define la responsabilidad del componente

Ejemplo: queremos un componente ProductList que muestre productos y permita seleccionar uno. Decidimos que ProductList no guardará la selección internamente; solo la mostrará y notificará cambios.

Paso 2: lista entradas (props) mínimas

  • items: lista de productos a renderizar.
  • selectedId: id del producto seleccionado (controlado por el padre).
  • onSelect: callback cuando el usuario selecciona uno.
function ProductList({ items, selectedId, onSelect }) {  return (    <ul>      {items.map((p) => (        <li key={p.id}>          <button            type="button"            aria-pressed={p.id === selectedId}            onClick={() => onSelect(p.id)}          >            {p.name}          </button>        </li>      ))}    </ul>  );}

Paso 3: define qué delega (eventos) y nómbralos bien

Convención común: callbacks empiezan por on y describen el evento, no la implementación: onSelect (bien) vs setSelectedId (demasiado acoplado a cómo el padre guarda estado).

Paso 4: documenta mentalmente estados y casos límite

¿Qué pasa si items está vacío? ¿Se permite selectedId nulo? Puedes reflejarlo en el render.

function ProductList({ items, selectedId, onSelect }) {  if (!items.length) return <p>No hay productos.</p>;  return (    <ul>      {items.map((p) => (        <li key={p.id}>          <button            type="button"            aria-pressed={p.id === selectedId}            onClick={() => onSelect(p.id)}          >            {p.name}          </button>        </li>      ))}    </ul>  );}

Flujo unidireccional y elevación de datos (lifting state up)

Cuando dos componentes hermanos necesitan compartir o coordinar datos, el flujo unidireccional te empuja a una consecuencia natural: elevar el estado al ancestro común más cercano y pasar datos hacia abajo como props.

Ejemplo: lista + detalle sincronizados

Queremos que al seleccionar un producto en la lista, se actualice un panel de detalle. La selección debe vivir en el padre.

function ProductDetail({ product }) {  if (!product) return <p>Selecciona un producto.</p>;  return (    <div>      <h4>{product.name}</h4>      <p>Precio: {product.price}€</p>    </div>  );}
function CatalogPage({ products }) {  const [selectedId, setSelectedId] = React.useState(null);  const selected = products.find((p) => p.id === selectedId) ?? null;  return (    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>      <ProductList        items={products}        selectedId={selectedId}        onSelect={setSelectedId}      />      <ProductDetail product={selected} />    </div>  );}

Aquí se ve el patrón completo: datos bajan (selectedId, selected) y eventos suben (onSelect). ProductList no necesita saber nada de useState; solo dispara onSelect.

Controlado vs no controlado (decisión de API)

Un componente es controlado cuando su valor viene por props y se actualiza mediante callbacks (como selectedId + onSelect). Es no controlado cuando maneja su propio estado interno. Para componentes reutilizables, una API controlada suele ser más flexible cuando el estado debe coordinarse con otros.

EnfoqueVentajaCuándo usar
ControladoSincronización y predictibilidadFormularios, filtros, selección compartida
No controladoUso simple, menos propsWidgets aislados, prototipos, estado local sin dependencias

Legibilidad de props: prácticas recomendadas

Evitar “prop drilling” temprano (sin sobrerreaccionar)

Prop drilling es pasar props a través de varios niveles de componentes que no las usan, solo para llegar a un descendiente. No es “malo” por sí mismo, pero se vuelve frágil cuando:

  • Hay muchos niveles intermedios.
  • Se pasan muchas props juntas.
  • Los componentes intermedios cambian a menudo.

Recomendación práctica: si el drilling es de 1–2 niveles y pocas props, mantenlo simple. Si empieza a crecer, considera alternativas como componer de otra forma (pasar el componente hijo como children), elevar el estado a un punto más cercano o usar un contexto (si encaja con el alcance global o semiglobal).

Agrupar props relacionadas en un objeto (cuando tenga sentido)

Si varias props viajan juntas y representan una entidad, agrúpalas para reducir ruido y mejorar coherencia.

// Demasiadas props sueltasfunction ShippingAddress({ street, city, zip, country }) { /* ... */ }
// Mejor: un objeto con significadofunction ShippingAddress({ address }) {  return (    <address>      {address.street}, {address.city} {address.zip}, {address.country}    </address>  );}

Equilibrio: no agrupes todo “porque sí”. Si el componente suele necesitar solo una parte, agrupar puede forzar renders y acoplar más de la cuenta. Agrupa cuando el conjunto es coherente y se usa como unidad.

Preferir nombres orientados a intención

  • onRemove mejor que handleRemove (el padre no “maneja”, el hijo emite).
  • isLoading, hasError mejor que loading, error si son booleanos.
  • variant mejor que múltiples flags como primary, secondary, danger (evita combinaciones inválidas).

Ejemplos de diseño de API de componentes

API 1: componente de entrada de texto (controlado) con configuración clara

function TextField({  label,  value,  onChange,  placeholder = "",  helperText,  error = null,  disabled = false,}) {  return (    <label style={{ display: "grid", gap: 6 }}>      <span>{label}</span>      <input        value={value}        onChange={(e) => onChange(e.target.value)}        placeholder={placeholder}        disabled={disabled}        aria-invalid={Boolean(error)}      />      {error ? <small style={{ color: "crimson" }}>{error}</small> : null}      {!error && helperText ? <small>{helperText}</small> : null}    </label>  );}

Contrato: recibe value y onChange (controlado), opciones de UI (placeholder, disabled) y mensajes (helperText, error). No decide validaciones; solo las muestra.

API 2: componente de lista con render prop (cuando children no basta)

Si necesitas personalizar cómo se renderiza cada item, una opción es pasar una función.

function List({ items, renderItem, emptyState = null }) {  if (!items.length) return emptyState;  return <ul>{items.map(renderItem)}</ul>;}
<List  items={products}  emptyState={<p>Sin resultados</p>}  renderItem={(p) => (    <li key={p.id}>{p.name} — {p.price}€</li>  )}/>

Contrato: items + renderItem. Esto evita crear un componente nuevo solo para cambiar el render de cada elemento.

API 3: componente “contenedor” con children y callbacks mínimos

Ejemplo: un modal que controla estructura y accesibilidad, pero delega el contenido.

function Modal({ isOpen, title, onClose, children }) {  if (!isOpen) return null;  return (    <div role="dialog" aria-modal="true" className="modal">      <div className="modal__panel">        <header className="modal__header">          <h4>{title}</h4>          <button type="button" onClick={onClose}>Cerrar</button>        </header>        <div className="modal__body">{children}</div>      </div>    </div>  );}

Contrato: el padre controla isOpen; el modal emite onClose; el contenido es libre vía children.

Ahora responde el ejercicio sobre el contenido:

En un componente React que recibe props, ¿cuál es la forma correcta de reflejar un cambio en un valor que viene del padre, respetando la inmutabilidad y el flujo de datos unidireccional?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Las props se tratan como entradas inmutables: el hijo no debe cambiarlas. Para actualizar un valor controlado por el padre, el hijo emite un evento mediante un callback y el padre actualiza su estado y vuelve a pasar nuevas props (datos bajan, eventos suben).

Siguiente capítulo

Estado en React: modelado de datos cambiantes y renderizado reactivo

Arrow Right Icon
Portada de libro electrónico gratuitaReact para principiantes: mentalidad de componentes y manejo de estado
25%

React para principiantes: mentalidad de componentes y manejo de estado

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.