3 Reglas que debes recordar para crear componentes React realmente reutilizables.

La composición en React es el corazón del framework y es muy curioso cómo es menospreciada tanto.

Es tan fácil crear componentes en React, que olvidamos esforzarnos un poco por crear componentes que puedan ser usados en la composición de otros, que puedan reutilizarse fácilmente y que puedan convertirse en el fundamento de nuevos componentes más complejos que no haremos nosotros, que harán otros programadores.

Cuando programamos en un pequeño grupo o de forma individual, olvidamos que debemos programar y producir componentes para otros developers, que esa debe ser la premisa de nuestra calidad.

Por eso, en este post vamos a pasar por 3 reglas básicas para crear componentes verdaderamente resuables y vamos a aprender estas 3 regleas básicas construyendo un botón que puedas reusar en todos tus proyectos React + Tailwind, de ahora en adelante.

Regla #1: Recuerda utilizar children lo más posible

Podríamos pensar en colocar props para colocar iconos, ya sea a la izquierda o a la derecha, pero esto nos limitaría tal vez a utilizar una librería específica de iconos, y es mucho mejor permitir al developer emplear la librería que prefiera. Logramos esta libertad colocando props que reciban ReactNode en vez de strings, pero aún mejor podemos dejar al developer decidir el contenido del botón en totalidad. Aquí están las 2 opciones:

import { type  ReactNode } from  'react';

type  ButtonProps = {
    children: ReactNode;
    leftIcon?: ReactNode;
    rightIcon?: ReactNode;
};
export  default  function  Button({ children, leftIcon, rightIcon }: ButtonProps) {
    return (
        <button  className='flex'>
            {leftIcon}
            {children}
            {rightIcon}
        </button>
    );
}

En este ejemplo, podemos ver que recibimos de forma opcional los iconos tanto de la izquierda como de la derecha, y son opcionales principalmente porque también podemos incluir estos iconos en children. Todos son ReactNode, lo que facilita la composición.

Regla #2: Los props booleanos deben muy semánticos

Hay un grupo de propiedades que un boton posee, que también queremos recibir en nuestro componente, estos valores son booleanos y los nombres de las propiedades no son tan semánticos como nos gustaría, pero estamos generando nuestro propio componente y podemos hacer más obvio el nombre de estos props, así que ¡hagámoslo!

import { type ReactNode } from 'react';

type ButtonProps = {
  children: ReactNode;
  leftIcon?: ReactNode;
  rightIcon?: ReactNode;

  isDisabled?: boolean;
  type?: 'submit' | 'button';
};
export default function Button({
  children,
  leftIcon,
  rightIcon,

  isDisabled,
  type = 'button',
}: ButtonProps) {
  return (
    <button disabled={isDisabled} type={type} className='flex'>
      {leftIcon}
      {children}
      {rightIcon}
    </button>
  );
}

Observa cómo hemos convertido la propiedad disabled en isDisabled para nuestro componente. También agregamos el prop opcional type, de esta forma todos nuestros botones estarán estandarizados y podremos ser explícitos con un botón que es parte de un formulario.

Hay algo más que quiero agregar a nuestro botón, me gustaría darle la oportunidad de mostrar un estado de cargo con el prop isLoading para que muestre un spinner e incluso gregar el prop loadingText por si el developer necesita mostrar un texto personalizado cuando isLoading es true

import { type ReactNode } from 'react';

type ButtonProps = {
  children: ReactNode;
  leftIcon?: ReactNode;
  rightIcon?: ReactNode;

  isDisabled?: boolean;
  type?: 'submit' | 'button';
  isLoading?: boolean;
  loadingText?: string | null;
};
export default function Button({
  children,
  leftIcon,
  rightIcon,

  isDisabled,
  type = 'button',

  isLoading,
  loadingText = null,
}: ButtonProps) {
  return (
    <button disabled={isDisabled} type={type} className='flex'>
      {isLoading && (
        <div className='w-8 h-8 border border-t-2 border-t-blue-500 animate-spin rounded-full' />
      )}
      {!isLoading && leftIcon}
      {isLoading ? loadingText : children}
      {!isLoading && rightIcon}
    </button>
  );
}

Estos son todos los props que vamos a utilizar, observa como el estado de loading modifica el contenido del botón al mismo tiempo que involucra el loadingText, no hay problema si el texto no se recibe pues resultará en null

Regla #3: Los estilos se evitan, pero pueden modificarse.

Para terminar nuestro botón, vamos a permitirle al developer decidir qué colores se usarán tanto para el background como para el outline y el loading spinner.

¡Ojo! 👀 en un proyecto con un equipo dividido entre diseñadores y developer o incluso en un equipo mediano, no es buena idea permitir los estilos de tus componentes, estos estilos deberían ser fijos y definidos dentro de un design system, pero si los componentes son para tus propios proyectos, bueno, si querras manipular los estilos.

import { useMemo, type ReactNode } from 'react';

type ButtonProps = {
  children: ReactNode;
  leftIcon?: ReactNode;
  rightIcon?: ReactNode;

  isDisabled?: boolean;
  type?: 'submit' | 'button';
  isLoading?: boolean;
  loadingText?: string | null;

  bgColor?: string;
  outlineColor?: string;
};
export default function Button({
  children,
  leftIcon,
  rightIcon,

  isDisabled,
  type = 'button',

  isLoading,
  loadingText = null,

  bgColor = 'blue-500',
  outlineColor = 'blue-800',
}: ButtonProps) {
  const classname = useMemo(() => {
    return `flex gap-2 m-2 rounded-md bg-${bgColor} px-4 py-2 text-white outline-${outlineColor} transition-all hover:scale-105 active:scale-100`;
  }, [outlineColor, bgColor]);

  const spinnerClassname = `w-8 h-8 border border-t-2 border-t-${outlineColor} animate-spin rounded-full`;

  return (
    <button
      className={classname}
      disabled={isDisabled}
      type={type}
    >
      {isLoading && <div className={spinnerClassname} />}
      {!isLoading && leftIcon}
      {isLoading ? loadingText : children}
      {!isLoading && rightIcon}
    </button>
  );
}

Este es un aproach donde tú decides que partes de los estilos le permites al developer manipular, en nuestro caso solo le permitimos decidir sobre el background y el outline como colores más importantes. Pero no le estamos permitiendo decidir sobre espacios, tamaños y más posibilidades, nos inundaríamos de props solo para estilos y por más que nos esforzaramos en ser flexibles no lograríamos serlo lo suficiente. Por eso es mejor permitirle al developer controlar el prop className por completo:

import { useMemo, type ReactNode } from 'react';

type ButtonProps = {
  children: ReactNode;
  leftIcon?: ReactNode;
  rightIcon?: ReactNode;

  isDisabled?: boolean;
  type?: 'submit' | 'button';
  isLoading?: boolean;
  loadingText?: string | null;

  bgColor?: string;
  outlineColor?: string;

  className?: string;
};
export default function Button({
  children,
  leftIcon,
  rightIcon,

  isDisabled,
  type = 'button',

  isLoading,
  loadingText = null,

  bgColor = 'blue-500',
  outlineColor = 'blue-800',

  className,
}: ButtonProps) {
  const classname = useMemo(() => {
    return `flex gap-2 m-2 rounded-md bg-${bgColor} px-4 py-2 text-white outline-${outlineColor} transition-all hover:scale-105 active:scale-100`;
  }, [outlineColor, bgColor]);

  const spinnerClassname = `w-8 h-8 border border-t-2 border-t-${outlineColor} animate-spin rounded-full`;

  return (
    <button
      className={classname + ' ' + className}
      disabled={isDisabled}
      type={type}
    >
      {isLoading && <div className={spinnerClassname} />}
      {!isLoading && leftIcon}
      {isLoading ? loadingText : children}
      {!isLoading && rightIcon}
    </button>
  );
}

En este punto, hay muchas decisiones que dependen del estilo, prácticas y hábitos del developer, recuerda que no existe una sola verdad.

También en este punto tienes un botón reusable que además es personalizable y que puedes utilizar con confianza en cualquiera de tus proyectos.

Si te vez en la necesidad de modificar el prop className frecuentemente, considera convertirla en una variante.

De esta forma puedes tener 2 o 3 className predefinidas que uses en todo tu sitio web y solo para el prop variant con el nombre correspondiente y aplicar la className correspondiente, evitándote pasar todo el string del className.

¡Y ya está! has creado tu primer botón reutilizable con composición básica y Tailwind. Si te gustaría producir más componentes conmigo, no dejes de decírmelo en mi Twitter y con gusto puedo generar el componente que estés necesitando. No olvides suscribirte al newsletter para enterarte de los siguientes componentes.

Checa el resultado

Checa el resultado en Codesandbox

Abrazo. Bliss.

Did you find this article valuable?

Support Héctor BlisS by becoming a sponsor. Any amount is appreciated!