Problemas con los props de mis componentes

Al querer crear componentes que sean extremadamente reutilizables podemos caer en la trampa de crear una API más grande de lo normal que soporte todos nuestros casos de uso, y peor aún, posibles futuros cambios (Optimización Prematura). En este post explicaré a través de un ejemplo cómo nuestro componente terminará en un estado indeseable y cómo podemos remediarlo.

Nuestro ejemplo utilizará dos componentes: UserCard y IconCard

Estos, a su vez, están compuestos por cuatro componentes más pequeños a los que llamaré, Componentes Base, Title, Description, Avatar y Icon.

Cada uno de esos componentes, tiene una sola responsabilidad bien definida y la hace bien, ya sea mostrar texto con ciertos estilos, mostrar un avatar encerrado en un círculo o mostrar un ícono SVG. Decidí omitir ciertas partes del código como estilos para que el ejemplo sea mucho más fácil de entender:

function Title({ children }) {
  return <h1>{children}</h1>;
}

function Description({ children }) {
  return <p>{children}</p>;
}

function Avatar({ src, alt }) {
  return <Image src={src} alt={alt} />;
}

function Icon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 50 50"
      width="50"
      height="50"
    >
      <circle fill="red" stroke="black" cx="25" cy="25" r="20" />
    </svg>
  );
}

Creemos un componente llamado TextBlock, este componente usará Title y Description.

function TextBlock({ title, description }) {
  return (
    <div>
      <Title>{title}</Title>
      <Description>{description}</Description>
    </div>
  );
}

Ahora, creemos los componente UserCard y IconCard que mencionamos al principio del artículo:

function UserCard({ title, description, src, alt }) {
  return (
    <div>
      <Avatar src={src} alt={alt} />
      <TextBlock title={title} description={description} />
    </div>
  );
}

function IconCard({ title, description }) {
  return (
    <div>
      <Icon />
      <TextBlock title={title} description={description} />
    </div>
  );
}

Ahora, supongamos que tenemos nuevos requerimientos para nuestros componentes:

  • UserCard debe tener un título rojo

  • IconCard debe tener un título azul

  • TextBlock debe seguir con sus colores originales

    Este es un momento clave, ya que estos requerimientos afectan directamente la API de nuestros componentes

La manera más fácil de agregar estos cambios es agregar "un prop más" (className) a TextBlock para poder agregar estilos desde el componente padre

/**
 * New prop: className
 */
function TextBlock({ title, description, className }) {
  return (
    <div>
      <Title className={className}>{title}</Title>
      <Description>{description}</Description>
    </div>
  );
}

Este cambio nos permitirá modificar el color del título de TextBlock a nuestro antojo.

Ahora, supongamos que debemos cambiar la descripción de TextBlock (Description) a nuestro antojo. Utilicemos la misma solución del cambio anterior, agregar "un prop más". A este prop lo vamos a llamar descriptionClassName.

/**
 * New prop: descriptionClassName
 */
function TextBlock({ title, description, className, descriptionClassName }) {
  return (
    <div>
      <Title className={className}>{title}</Title>
      <Description className={descriptionClassName}>{description}</Description>
    </div>
  );
}

Ahora, ¿qué pasa si queremos agregar un espacio diferente entre el título y la descripción de IconCard y UserCard? Sigamos el patrón de los dos cambios anteriores.

function TextBlock({
  title,
  description,
  className,
  descriptionClassName,
  customSpaceClassName,
}) {
  return (
    <div>
      <Title className={`${className} ${customSpaceClassName}`}>{title}</Title>
      <Description className={descriptionClassName}>{description}</Description>
    </div>
  );
}

Solo puedo ver con horror cómo terminó nuestro componente TextBlock, pasó de ser un componente que solo le importaba recibir 2 valores:

  1. title
  2. description

A recibir 5 valores:

  1. title
  2. description
  3. className para cambiar el color de Title
  4. descriptionClassName para agregar un color a Description
  5. customSpaceClassName para agregar un espacio entre Title y Description

Ahora TextBlock es un componente que ya no hace una sola cosa y la hace bien, ahora es un componente con multiples responsabilidades (sobre todo visuales) y es mucho más difícil de entender, probar y usar en un futuro.

Pensando a futuro, se me vienen a la cabeza varias preguntas como:

  1. ¿Cómo podemos saber qué hace cada uno de sus props?
  2. ¿Cómo sabemos qué props omitir o no?
  3. ¿Cuáles son los defaults de cada uno de las props que estoy pasando?

En la práctica, usaríamos nuestro componente de esta manera:

function UserCard({ title, description, src, alt }) {
  return (
    <div>
      <Avatar src={src} alt={alt} />
      <TextBlock
        title={title}
        description={description}
        className="red-title"
        descriptionClassName="green-description"
        spaceClassName="big-space"
      />
    </div>
  );
}

function IconCard({ title, description }) {
  return (
    <div>
      <Icon />
      <TextBlock
        title={title}
        description={description}
        className="blue-title"
        descriptionClassName="yellow-description"
        spaceClassName="medium-space"
      />
    </div>
  );
}

Con esto llego al fin del ejemplo. Podemos concluir que:

  1. Algunas abstracciones no envejecen de buena manera.
  2. Entre más props tenga nuestro componente (superficie más grande) más complicado va a ser de utilizar, cambiar y probar.
  3. No porque ya tengamos un componente que hace algo parecido a lo que necesitamos, significa que debamos modificarlo de acuerdo a nuestro nuevo caso de uso.

La solución

Para evitar que nuestros componentes lleguen a un estado como el de TextBlock debemos tener en cuenta las siguientes recomendaciones:

  1. Pensar dos veces antes de agregar "un prop más" por más tentador que suene. Siempre va a ser más costoso quitar algo que no sirve, que nunca agregarlo.
  2. Si TextBlock es una componente universal en nuestra aplicación, podemos negociar este cambio con los responsables del diseño del producto (Diseño/Product Owners), es mucho más fácil y rápido hacer un cambio del diseño en Figma o Sketch que en código.
  3. Si nuestra negociación no da frutos, mi recomendación sería no usar TextBlock en ninguno de los casos, y empezar a usar los componentes base Title y Description. Esto se va a traducir en código duplicado, pero va a ser mucho más fácil de cambiar y el componente universal TextBlock seguirá intacto.

En la práctica, nuestros componentes UserCard y IconCard se verán así:

function UserCard({ title, description, src, alt }) {
  return (
    <div>
      <Avatar src={src} alt={alt} />
      <Title className="red-title big-space">{title}</Title>
      <Description className="blue-description">{description}</Description>
    </div>
  );
}

function IconCard({ title, description }) {
  return (
    <div>
      <Icon />
      <Title className="blue-title medium-space">{title}</Title>
      <Description className="yellow-description">{description}</Description>
    </div>
  );
}

Conclusión

  • Cuando enfrentemos situaciones en donde parece más fácil/rápido agregar "un prop más", pensemos que ese nuevo prop puede significar que nuestro componente sea menos mantenible, más difícil de usar y entender en un futuro.
  • Tener código duplicado no está mal, recuerden que tener código duplicado siempre va a ser más barato que una mala abstracción.
  • Un solo componente no debe adaptarse a todos y cada uno de los casos de uso de nuestro negocio. A veces está bien no usar ciertos componentes (abstracciones) existentes en nuestra aplicación.

Links relacionados con el tema

Suscríbete al newsletter

Solo lo necesito para enviarte información sobre mi blog y/o el contenido que cree.

© 2020-2021 Alejandro Nanez. All rights reserved.