React
Props
Explica cómo las props comunican datos y callbacks entre componentes, su carácter de solo lectura, valores por defecto, destructuring y diseño de APIs claras.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
React
Explica cómo las props comunican datos y callbacks entre componentes, su carácter de solo lectura, valores por defecto, destructuring y diseño de APIs claras.
Las props son las entradas con las que un componente recibe información y comportamiento desde su padre. Funcionan como el contrato público del componente: indican qué necesita, qué variaciones admite y cómo puede comunicar acciones hacia arriba.
Un componente de React no debería depender de variables ocultas para saber qué mostrar. Recibe datos mediante props y calcula su interfaz a partir de ellos.
type ProductCardProps = {
name: string;
price: number;
isAvailable: boolean;
};
function ProductCard({ name, price, isAvailable }: ProductCardProps) {
return (
<article>
<h2>{name}</h2>
<p>
{price.toLocaleString("es-CO", {
style: "currency",
currency: "COP",
})}
</p>
<p>{isAvailable ? "Disponible" : "Agotado"}</p>
</article>
);
}Cada vez que el padre renderiza <ProductCard />, crea una instancia configurada con valores concretos:
<ProductCard
name="Teclado mecánico"
price={280000}
isAvailable={true}
/>El mismo componente puede representar productos distintos sin duplicar su estructura.
Sin props, un componente reutilizable tendría que leer datos globales, importar valores concretos o contener información fija. Eso lo volvería difícil de reutilizar, probar y razonar.
Las props permiten separar dos responsabilidades:
padre
├── posee o consigue los datos
├── decide qué instancia crear
└── responde a las acciones
↓
props
↓
hijo
├── representa la información
├── aplica reglas visuales locales
└── comunica intencionesEl padre conoce el contexto. El hijo conoce cómo representar una parte de la interfaz.
Conceptualmente, este JSX:
<Welcome name="Nicolás" unreadCount={3} />le entrega a React una descripción equivalente a una llamada configurada con un objeto de props:
{
name: "Nicolás",
unreadCount: 3,
}React ejecuta el componente cuando necesita calcular su salida:
type WelcomeProps = {
name: string;
unreadCount: number;
};
function Welcome({ name, unreadCount }: WelcomeProps) {
return (
<header>
<h1>Hola, {name}</h1>
<p>{unreadCount} mensajes pendientes</p>
</header>
);
}Las props pertenecen a ese render. No son una caja mutable que el hijo pueda actualizar.
Un componente debe tratar las props como valores inmutables durante render.
function UserBadge({ user }: { user: User }) {
user.name = user.name.toUpperCase(); // Incorrecto
return <span>{user.name}</span>;
}Aunque JavaScript permita modificar el objeto, hacerlo rompe el contrato de React:
La alternativa es calcular un valor nuevo sin mutar la entrada:
function UserBadge({ user }: { user: User }) {
const displayName = user.name.toUpperCase();
return <span>{displayName}</span>;
}“Solo lectura” no significa que el objeto esté congelado por JavaScript. Significa que el componente debe respetar la propiedad del dato y no modificarlo.
En React, los datos normalmente fluyen desde componentes superiores hacia inferiores.
function Cart() {
const [quantity, setQuantity] = useState(1);
return (
<QuantitySelector
value={quantity}
onChange={setQuantity}
/>
);
}type QuantitySelectorProps = {
value: number;
onChange: (nextValue: number) => void;
};
function QuantitySelector({ value, onChange }: QuantitySelectorProps) {
return (
<div>
<button
type="button"
onClick={() => onChange(Math.max(1, value - 1))}
>
Restar
</button>
<output>{value}</output>
<button type="button" onClick={() => onChange(value + 1)}>
Sumar
</button>
</div>
);
}El flujo es:
Cart conserva el estado.value.onChange.El hijo no “sube datos” mágicamente. Ejecuta una función que el padre le entregó.
Una callback prop funciona mejor cuando comunica una intención, no cuando expone innecesariamente la implementación interna del padre.
Menos expresivo:
<TodoItem todo={todo} setTodos={setTodos} />Más claro:
<TodoItem todo={todo} onDelete={handleDeleteTodo} />type TodoItemProps = {
todo: Todo;
onDelete: (todoId: string) => void;
};
function TodoItem({ todo, onDelete }: TodoItemProps) {
return (
<li>
<span>{todo.title}</span>
<button type="button" onClick={() => onDelete(todo.id)}>
Eliminar
</button>
</li>
);
}onDelete expresa qué ocurrió. El padre conserva libertad para actualizar estado local, llamar una API, mostrar una confirmación o registrar métricas.
TypeScript permite expresar qué entradas necesita el componente:
type AvatarProps = {
src: string;
alt: string;
size?: number;
};
function Avatar({ src, alt, size = 48 }: AvatarProps) {
return (
<img
src={src}
alt={alt}
width={size}
height={size}
/>
);
}src y alt son obligatorias.size es opcional.size, se utiliza 48.El valor predeterminado se aplica cuando la prop es undefined, no cuando es null:
<Avatar src="/nico.webp" alt="Nicolás" size={undefined} /> // usa 48
<Avatar src="/nico.webp" alt="Nicolás" size={null} /> // error de tiposEvita usar valores predeterminados para ocultar entradas realmente necesarias. Una prop obligatoria comunica mejor un requisito del componente.
Las props no son únicamente parámetros; forman la API pública del componente.
Una API débil puede permitir estados contradictorios:
type AlertProps = {
success?: boolean;
warning?: boolean;
danger?: boolean;
};¿Qué ocurre si las tres llegan como true?
Una variante limita las combinaciones:
type AlertProps = {
tone: "success" | "warning" | "danger";
title: string;
children: React.ReactNode;
};function Alert({ tone, title, children }: AlertProps) {
return (
<section className={`alert alert--${tone}`}>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}Una buena API de props:
Una prop puede recibir diferentes tipos de información. La decisión cambia quién controla la representación.
<UserCard user={user} />El hijo decide cómo mostrar el usuario. Es útil cuando quieres una representación consistente.
<Panel
header={<OrderHeader order={order} />}
footer={<OrderActions orderId={order.id} />}
/>El padre controla regiones específicas. Esto funciona como composición mediante slots.
<DataList
items={products}
renderItem={(product) => <ProductRow product={product} />}
/>Es útil cuando el componente controla el proceso, pero el consumidor controla cómo representar cada resultado. También añade complejidad; no debe ser la opción predeterminada cuando children o una prop de elemento son suficientes.
type CardProps = {
children: React.ReactNode;
};
function Card({ children }: CardProps) {
return <section className="card">{children}</section>;
}<Card>
<h2>Pedido confirmado</h2>
<p>Tu compra fue registrada correctamente.</p>
</Card>React entrega el contenido entre las etiquetas mediante children. No asumas que siempre es un único elemento: puede ser texto, varios elementos, un fragment, un array o ausencia de contenido.
La nota Composición con children y slots profundiza en cómo diseñar estas APIs.
Puedes reenviar varias props con el operador spread:
type TextInputProps = React.ComponentProps<"input">;
function TextInput({ className, ...inputProps }: TextInputProps) {
return (
<input
{...inputProps}
className={`text-input ${className ?? ""}`}
/>
);
}Esto es útil para wrappers transparentes de elementos nativos, porque conserva atributos como name, required, disabled, aria-* y handlers.
Sin embargo, el orden importa:
<input type="email" {...props} />Aquí props.type podría reemplazar email.
<input {...props} type="email" />Aquí el componente garantiza type="email".
El spreading puede ser una mala decisión cuando:
Objetos, arrays y funciones se comparan por referencia en JavaScript:
<ProductList filters={{ category: "tech" }} />El objeto se crea de nuevo en cada render. Eso no es incorrecto. Solo importa cuando una parte del sistema depende de conservar la identidad, por ejemplo:
memo.No uses useMemo o useCallback automáticamente para cada prop. Primero identifica un problema semántico o de rendimiento verificable.
Este patrón suele causar confusión:
function ProfileForm({ user }: { user: User }) {
const [name, setName] = useState(user.name);
// ...
}user.name solo se utiliza durante la inicialización. Si luego llega otro usuario, el estado no cambia automáticamente.
Eso puede ser correcto cuando el formulario representa un borrador independiente. Pero debes decidir qué comportamiento espera el producto:
<ProfileForm name={name} onNameChange={setName} /><ProfileForm key={user.id} user={user} />La key reinicia deliberadamente el estado cuando cambia la identidad del usuario.
Cuando no necesitas editarlo, no lo copies:
function UserName({ user }: { user: User }) {
return <p>{user.name.trim()}</p>;
}Copiar props a state crea dos fuentes de verdad. Hazlo únicamente cuando exista una diferencia real entre “valor recibido” y “estado local editable”.
TypeScript verifica usos conocidos durante desarrollo:
<ProductCard name="Mouse" price="120000" /> // error de tiposPero los tipos desaparecen al ejecutar el programa. Si los datos vienen de una API, localStorage o parámetros de URL, necesitas validarlos en runtime antes de convertirlos en props confiables.
const product = ProductSchema.parse(apiResponse);
return <ProductCard name={product.name} price={product.price} />;Las props tipadas protegen el contrato interno; no vuelven confiable una fuente externa.
Una prop obligatoria debería producir un error de TypeScript en desarrollo. En JavaScript puro, el componente podría recibir undefined; diseña fallbacks solo cuando tengan sentido para el producto.
No uses optional chaining en todas partes para esconder datos incompletos:
<p>{user?.profile?.name ?? ""}</p>A veces eso evita un crash, pero también puede ocultar que el contrato está roto. Valida el dato en el límite adecuado.
El hijo puede llamar onSave, pero normalmente no debe asumir que la operación siempre tendrá éxito. El padre puede comunicar estados como isSaving, error o devolver una promesa cuando el contrato lo requiera.
Si alguien muta un objeto enviado como prop sin actualizar state, React puede no saber que debe renderizar de nuevo. Las actualizaciones deben producir nuevas referencias mediante el mecanismo responsable del estado.
Ocurre al tratar un objeto recibido como si perteneciera al hijo. Puede modificar datos compartidos sin provocar el render esperado. Corrígelo creando un nuevo valor y solicitando la actualización al propietario.
Expone demasiado del estado del padre y acopla el hijo a su implementación. Prefiere callbacks de intención como onDelete, onSelect o onQuantityChange.
Permite combinaciones contradictorias. Sustituye grupos de flags por variantes o uniones discriminadas.
El componente pierde su contrato y los errores aparecen en runtime. Modela el dato mínimo que realmente necesita.
Crea datos redundantes y problemas de sincronización. Define primero quién posee el valor y cuándo debe reiniciarse.
Enviar un objeto completo por comodidad puede acoplar el componente a campos que no necesita. A veces conviene pasar un modelo reducido; otras veces mantener el objeto cohesivo es más claro. La decisión depende de la responsabilidad, no de una regla universal.
Usa props para:
children.Las props dejan de ser cómodas cuando datos globales deben atravesar muchas capas que no los utilizan. En ese caso evalúa composición, Context o un store externo.
No obstante, evitar dos o tres niveles de props no justifica automáticamente crear estado global. El flujo explícito suele ser más fácil de seguir y probar.
children es una prop utilizada para composición.setTodos y pasar onDelete?useState(user.name) no se sincroniza automáticamente cuando cambia user?tone: "success" | "warning" | "danger" frente a tres booleanos?setTodos expone la implementación del padre; onDelete comunica una intención y mantiene el contrato desacoplado.useState se utiliza al inicializar esa instancia del componente, no como una sincronización continua.Composición con children y slots explica cómo diseñar componentes flexibles sin convertir su API en una colección de flags y detalles visuales.