React props: buenas prácticas para código más limpio y escalable

Publicado: Hace 8 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.

¿Qué son los props en React?

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.



Figura 1. ¿Qué son los props? Una lámpara es un ejemplo de un componente padre, donde utilizamos ciertos props para mantener la comunicación con sus componentes hijos. Por ejemplo, podemos encenderla/apagarla, nivelar la intensidad o cambiar el color de la luz.


Diferencia entre props y states

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

  • Inmutables.
  • Se definen desde el exterior del componente.
  • Sirven para comunicar datos del padre al hijo.


State

  • Pertenece al propio componente.
  • Cambia con el tiempo en respuesta a interacciones o eventos.
  • Permite que el componente administre su propio comportamiento interno.

¿Por qué es importante diseñar bien los props?

Evitar duplicación y código innecesario

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.

Mejora la reutilización de los componentes

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.

  • App. Se encarga de almacenar los productos que se van dando de alta y comunicar a los componentes ProductList y ProductForm.
  • ProductList. Su única responsabilidad es listar los productos. No le importa qué mecanismo se use para darlos de alta, solo recibe los productos como props.
  • ProductForm. Su única responsabilidad es crear un producto. Tampoco le importa qué harán con ese producto después, solo se limita a generarlo y pasarlo hacia arriba mediante un prop.

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.

Facilitar las pruebas y el mantenimiento

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:

  • Validan que los props cumplen su responsabilidad. Aseguran que cada prop tenga un propósito claro y no existan props innecesarios.
  • Previenen cambios inesperados en la API del componente. Si un prop cambia de nombre o de tipo, las pruebas fallan y alertan del impacto.
  • Garantizan la comunicación correcta entre componentes. Verifican que los props viajen de un componente padre a uno hijo tal como se espera.
  • Facilitan el refactor de props. Permiten renombrar, eliminar o dividir props con confianza, sabiendo que el comportamiento esperado está cubierto.


Si quieres saber más sobre pruebas te invito a revisar los siguientes enlaces:


Buenas prácticas al trabajar con props en React

Usa nombres descriptivos y consistentes

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:


  • Sea descriptivo. indica claramente lo que hace (responde a un cambio).
  • Sea consistente. Se use con el mismo propósito en todos los componentes que lo requieran.

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;

Define tipos de props con Proptypes o TypeScript

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;

Establece valores por defecto

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:


  • Props requeridos. Son esenciales para que el componente cumpla su función.
  • Props opcionales. Si no se pasan, no afectan la funcionalidad principal. En estos casos, establecer un valor por defecto permite mantener un comportamiento coherente y dejar claro que ese valor puede ser modificado para personalizar el resultado.


 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.

Evita props innecesarios o demasiado genéricos

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:


  • Props tipo data, config, options con muchos campos internos.
  • Varias banderas que se pisan entre si (primary, secondary, danger, success)
  • Callbacks ambiguos (onAction, onEvento) que no comunican la intención.

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" />

Agrupa props relacionados en objetos

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


  1. Define la responsabilidad del componente. ¿Qué promete hacer y qué NO?
  2. Empieza con lo mínimo. Pasa solo los props imprescindibles; deja que el componente “pida”más conforme crece.
  3. Cuando la lista crezca, categoriza. Agrupa props por dominio lógico (p. ej. user, ui, actions, validation).
  4. Prueba los comportamientos clave. Valida cómo cambia el componente según el objeto que se pase, especialmente si hay lógica de negocio.

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: () => {} }}
/>

Errores comunes al definir props

Pasar demasiados props a un mismo componente

Este es uno de los problemas más comunes al desarrollar en React.


¿Por qué ocurre?


  • Porque no tenemos clara la responsabilidad de cada componente.
  • Porque abusamos de la “granularidad”: creamos componentes innecesariamente pequeños (por ejemplo, un componente que solo renderiza un texto)
  • Como resultado, el árbol de componentes se llena de intermediarios que dependen de demasiados props.

Esto hace que: 


  • El componente padre deba pasar un sinfín de props a sus hijos.
  • El código sea difícil de mantener.
  • Y si no hay pruebas, la frustración la librería crece (cuando en realidad el problema está en el diseño de componentes).

No validar los tipos de props

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.

Cuándo sí conviene validar los tipos de props?

Dependerá de factores como:


  • Tiempo de entrega. Si hay mucha presión, puede ser más útil priorizar pruebas y responsabilidades claras.
  • Tamaño del equipo. Cuanto más grande el equipo, más probable es que la validación de tipos prevenga errores y discusiones innecesarias.
  • Nivel de experiencia. En equipos con juniors, una validación explícita puede ser una guía extra.
  • Escalabilidad. En proyectos grandes, PropTypes o TypeScript ayudan a mantener consistencia y confianza en el código.

Cambiar props en lugar de mantenerlos inmutables

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: 


  • Úsalo para inicializar estado local del hijo y notifica los cambios al padre mediante un onChange. 

  • El padre es quien posee la fuente de la verdad y actualiza su propio estado.

 // 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;

Conclusiones

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


  • Responsabilidad primero. Define qué hace el componente; los props se derivan de eso.
  • Inmutabilidad. Los props se tratan como solo lectura. Usa initialX para inicializar estado local y onChange para notificar cambios al padre (quien conserva la “fuente de la verdad”).
  • Nombres consistentes. Un buen nombre evita documentación extra.
  • Evita lo genérico. Mejor props específicos que contenedores tipo data/config. Para variantes, usa un único variant="..." en lugar de múltiples flags.
  • Valores por defecto con criterio. Solo en props opcionales; los requeridos no deben “ocultarse” con defaults.
  • Pruebas como contrato. Tests que verifiquen flujo de datos y API del componente (y, si el equipo crece, considera TypeScript o PropTypes).