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.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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.
| Enfoque | Ventaja | Cuándo usar |
|---|---|---|
| Controlado | Sincronización y predictibilidad | Formularios, filtros, selección compartida |
| No controlado | Uso simple, menos props | Widgets 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
onRemovemejor quehandleRemove(el padre no “maneja”, el hijo emite).isLoading,hasErrormejor queloading,errorsi son booleanos.variantmejor que múltiples flags comoprimary,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.