Publicado: Hace 9 días
Una de las formas principales para comunicar componentes en nuestras aplicaciones React es utilizando props. Sin embargo, con frecuencia se descuida el diseño a la hora de definir las responsabilidades de cada componente.
Los props no se tratan únicamente de “pasar información”, sino de comunicar de forma clara la responsabilidad y el propósito del componente. Al programar nuestros componentes desde este enfoque, impacta de forma positiva en la calidad del código tanto para quien consuma el componente como para quien lo desarrollar.
En este artículos, veremos qué son los props, por qué es importante diseñarlos bien, buenas prácticas, errores comunicas y ejemplos prácticos.
Definición y proposito de los propsLos props (abreviatura de properties) son valores que pueden recibir nuestros componentes y se encargan de realizar alguna tarea en especifico. Son una de las formas más comunes de comunicar los componentes padres con los hijos. Para ejemplificar esto, veamos la siguiente figura 1.
Sigamos con el ejemplo de la Figura 1 (la lámpara). La lámpara completa actúa como el componente padre, y es este quien mantiene un state interno con la última configuración antes de apagarse, como la intensidad media, la luz cálida o si estaba encendida. Cuando la volvemos a conectar, ese state se restaura y se distribuye a los componentes hijos, como la bombilla, el interruptor o el regulador, mediante props que indican a cada parte cómo debe comportarse.
Diferencias principales
Props
State
Uno de los problemas más frecuentes en el desarrollo de software es la duplicación de código. Esta puede aparecer en pequeñas o grandes cantidades de bloques de código a lo largo del proyecto. El verdadero inconveniente no es que exista código repetido en sí, sino la falta de criterio para decidir cuándo refactorizarlo.
Con frecuencia, la repetición surge por factores como la presión de cumplir fechas de entrega, la falta de tiempo para atender la deuda técnica o simplemente porque copiar y pegar resulta más rápido que diseñar una solución reusable. Sin embargo, mientras más capas y dependencias añadimos a la aplicación, mayor es el riesgo de que se convierta en un sistema frágil y difícil de mantener. Un cambio aparentemente menor puede generar efectos colaterales en otras partes del código si no está bien estructurado.
De la misma manera, el código innecesario suele estar ligado a la duplicidad. ¿A qué nos referimos con esto? A que, en ocasiones, un copiar y pegar puede ser más efectivo que crear toda una capa extra para resolver un caso simple.
NOTA: No se trata de eliminar toda repetición a cualquier costo, sino de tener la conciencia para saber cuándo estamos agregando complejidad innecesaria.
Creo firmemente que el código, al igual que una enfermedad, habla por sí mismo: cuando algo empieza a sentirse mal, lo notas. Y en ese momento es cuando debemos atenderlo y refactorizar.
Al diseñar correctamente las responsabilidades de los componentes, los props se vuelven más sencillos de identificar, tanto en la cantidad que se necesitan como en la manera de nombrarlos.
Cuando un componente tiene una responsabilidad clara, los props no se sienten forzados ni arbitrarios: simplemente surgen de manera natural conforme el componente se va desarrollando.
Un ejemplo dice más que mil palabras. Supongamos que tenemos tres componentes: App, ProductList y ProductForm.
import React from "react"; function ProductList({ products }) { return ( <div> <h2>Lista de productos</h2> <ul> {products.map((p, index) => ( <li key={index}> {p.name} - ${p.price} </li> ))} </ul> </div> ); } export default ProductList;
import React, { useState } from "react"; function ProductForm({ onAddProduct }) { const [name, setName] = useState(""); const handleSubmit = (e) => { e.preventDefault(); if (!name) return; onAddProduct({ name }); setName(""); }; return ( <div> <h2>Agregar producto</h2> <form onSubmit={handleSubmit}> <input type="text" placeholder="Nombre" value={name} onChange={(e) => setName(e.target.value)} /> <button type="submit">Agregar</button> </form> </div> ); } export default ProductForm;
import React, { useState } from "react"; import ProductList from "./ProductList"; import ProductForm from "./ProductForm"; function App() { const [products, setProducts] = useState([]); const addProduct = (product) => { setProducts([...products, product]); }; return ( <div> <ProductForm onAddProduct={addProduct} /> <ProductList products={products} /> </div> ); } export default App;
En este ejemplo —bastante sencillo— podemos darnos cuenta de las responsabilidades que cada componente asume y de la importancia de mantenerlas bien delimitadas. Gracias a ello, los props no se sienten sin sentido alguno.
Las pruebas son un gran aliado que todos deberían conocer. Al principio puede parecer extraño “escribir código para revisar código”, pero adoptar un enfoque guiado por pruebas te guía a estructurar mejor tu aplicación. La mayor ventaja es la confianza: puedes modificar y refactorizar sin miedo a romper algo, porque las pruebas actúan como red flag y te ayudan a decidir cómo se comunica tu código.
En la práctica, las pruebas en componentes React:
Si quieres saber más sobre pruebas te invito a revisar los siguientes enlaces:
Hay un dicho muy conocido: “Una acción vale más que mil palabras”. En el desarrollo de software podemos aplicarlo de la siguiente manera: un buen nombre vale más que mil explicaciones.
Elegir nombres descriptivos y consistentes en nuestros componentes es fundamental para que el código sea entendible y predecible.
Por ejemplo, supongamos que tenemos dos componentes distintos y ambos necesitan exponer un prop para notificar cambios que ocurren dentro de ellos. El nombre natural para ese prop sería onChange. Lo importante es que este prop:
De esta manera, cuando alguien vea onChange en cualquier componente, sabrá de inmediato qué esperar de él.
Veamos un ejemplo sencillo para ilustrarlo:
function InputText({ value, onChange }) { return ( <input type="text" value={value} onChange={(e) => onChange(e.target.value)} /> ); }
function SelectOption({ options, value, onChange }) { return ( <select value={value} onChange={(e) => onChange(e.target.value)}> {options.map((opt, index) => ( <option key={index} value={opt}> {opt} </option> ))} </select> ); }
import { useState } from "react"; import InputText from "./InputText"; import SelectOption from "./SelectOption"; function App() { const [name, setName] = useState(""); const [color, setColor] = useState(""); return ( <div> <InputText value={name} onChange={setName} /> <SelectOption options={["Rojo", "Verde", "Azul"]} value={color} onChange={setColor} /> <p>Hola {name}, tu color favorito es {color}</p> </div> ); } export default App;
Una estrategia para mantener los props más claros es utilizar alguna librería de tipado. Estas herramientas se encargan de validar el formato y tipo de datos que recibe un componente, ayudándonos a detectar errores temprano.
Entre las más utilizadas encontramos PropTypes y TypeScript, ambas muy buenas soluciones. Sin embargo, hay que considerar que agregar una capa de tipado también implica una nueva capa de abstracción: si no se diseña correctamente, puede volver el código más difícil de entender.
Veamos un ejemplo sencillo con PropTypes:
import PropTypes from "prop-types"; function UserCard({ name, age, isActive }) { return ( <div> <h3>{name}</h3> <p>Edad: {age}</p> <p>{isActive ? "Activo" : "Inactivo"}</p> </div> ); } // Validación de props UserCard.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number.isRequired, isActive: PropTypes.bool }; // Valores por defecto UserCard.defaultProps = { isActive: false }; export default UserCard;
El mismo ejemplo pero usando TypeScript:
type UserCardProps = { name: string; age: number; isActive?: boolean; // opcional }; function UserCard({ name, age, isActive = false }: UserCardProps) { return ( <div> <h3>{name}</h3> <p>Edad: {age}</p> <p>{isActive ? "Activo" : "Inactivo"}</p> </div> ); } export default UserCard;
Los valores por defecto son de gran utilidad para evitar que nuestros componentes se rompan. Si un componente no recibe la data correcta, puede presentar un comportamiento inesperado o incluso fallar.
Sin embargo, no todos los props deberían definirse con valores por defecto:
Veamos el siguiente ejemplo para entenderlo mejor.
function Button({ label, type = "button", disabled = false }) { if (!label) { throw new Error("El prop 'label' es requerido en Button"); } return ( <button type={type} disabled={disabled}> {label} </button> ); } export default Button;
No es necesario validar manualmente cada prop requerido dentro del componente. En muchos casos, basta con pruebas que verifiquen la comunicación entre componentes y que los props lleguen en el formato correcto. Estas pruebas te dan confianza para refactorizar sin sobrecargar el código con validaciones internas.
Para equipos grandes, una librería de tipado (p. ej., TypeScript o PropTypes) puede complementar estas pruebas y reforzar el contrato entre componentes.
Cuando empezamos a programar solemos querer cubrir “todos los casos”. El resultado: props demasiado genéricos que se pierde el foco sobre la responsabilidad del componente. Si primero defines qué hace cada componente, los props surgen de forma natural y con nombres claros.
Algunas red flags para nos indican que nuestros props están mal diseñados:
Veamos el siguiente ejemplo en código para entenderlo mejor:
// Antipatrón (genérico y ambiguo) function Card({ data, config, onAction }) { return ( <div> <h3>{data.title}</h3> {config.showSubtitle && <p>{data.subtitle}</p>} <button onClick={() => onAction("edit", data.id)}>Editar</button> </div> ); } <Button primary danger success />
function Card({ title, subtitle, showSubtitle = false, onEdit }) { return ( <div> <h3>{title}</h3> {showSubtitle && <p>{subtitle}</p>} <button onClick={onEdit}>Editar</button> </div> ); } <Button variant="danger" />
En el punto anterior vimos que usar props demasiado genéricos complica el mantenimiento. Entonces, ¿cómo asegurar que los props-objeto estén bien definidos?
Guía práctica
Antes (props sueltos) <UserCard name="Ada" email="[email protected]" avatarUrl="/ada.png" isLoading={false} onEdit={() => {}} onDelete={() => {}} theme="dark" showEmail /> Después (props agrupados) <UserCard user={{ name: "Ada", email: "[email protected]", avatarUrl: "/ada.png" }} ui={{ theme: "dark", showEmail: true, isLoading: false }} actions={{ onEdit: () => {}, onDelete: () => {} }} />
Este es uno de los problemas más comunes al desarrollar en React.
¿Por qué ocurre?
Esto hace que:
No siempre es un error omitir la validación de props.
Como vimos antes, si seguimos buenas prácticas (tests, nombres claros y responsabilidades bien definidas), la validación estricta de tipos puede no ser imprescindible.
He visto aplicaciones donde se validan hasta casos obvios, por ejemplo:
MyComponent.propTypes = { contentText: PropTypes.string.isRequired }
Cuando, en realidad, el nombre contextText ya comunica de forma clara que se trata de un texto.
Dependerá de factores como:
Los props deben tratarse como de solo lectura. Si un hijo necesita “cambiar” un valor que viene por props (por ejemplo, price), no debe mutarlo; en su lugar:
// TarifasForm.jsx (MAL) function TarifasForm({ price }) { // Reasignar un prop o mutarlo rompe el contrato de inmutabilidad. // Ejemplo con objeto: // price.amount = price.amount * 1.2; // ❌ mutación // Ejemplo con primitivo: // price = price * 1.2; // ❌ reasignación local y confusa return ( <div> <p>Precio recibido: {price}</p> {/* Sin manera correcta de devolver el cambio al padre */} </div> ); }
// TarifasForm.jsx (BIEN) import { useEffect, useState } from "react"; function TarifasForm({ initialPrice, onChange }) { const [localPrice, setLocalPrice] = useState(initialPrice); // Si el padre cambia el precio mientras el formulario está abierto, // sincronizamos el estado local. useEffect(() => { setLocalPrice(initialPrice); }, [initialPrice]); const handleSave = () => { onChange(localPrice); // notificamos al padre }; return ( <div> <h3>Tarifas avanzadas</h3> <label> Precio: <input type="number" value={localPrice} onChange={(e) => setLocalPrice(parseFloat(e.target.value) || 0)} /> </label> <div> <button onClick={handleSave}>Guardar</button> </div> </div> ); } export default TarifasForm;
import { useState } from "react"; import TarifasForm from "./TarifasForm"; function ProductForm() { const [price, setPrice] = useState(100); const [showTarifas, setShowTarifas] = useState(false); return ( <div> <h2>Producto</h2> <label> Precio base: <input type="number" value={price} onChange={(e) => setPrice(parseFloat(e.target.value) || 0)} /> </label> <button onClick={() => setShowTarifas(true)}>Administrar tarifa</button> {showTarifas && ( <TarifasForm initialPrice={price} onChange={(newPrice) => setPrice(newPrice)} /> )} </div> ); } export default ProductForm;
Los props no son el punto de partida: son la consecuencia de un buen diseño de componentes. Cuando cada componente tiene una responsabilidad única, los props aparecen solos, con nombres precisos, tipos claros, y sin duplicar lógica.
Principios clave