@hectorbliss
La composición en React es el corazón de la librería 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.
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.
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 carga 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
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; onClick?:()=>void; className?: string; }; export default function Button({ children, leftIcon, rightIcon, isDisabled, type = 'button', isLoading, loadingText = null, bgColor = 'blue-500', outlineColor = 'blue-800', onClick, 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 onClick={onClick} 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 agregamos el evento onClick, que es el prop más importante para poder usar nuestro botón
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.
De esta forma puedes tener 2 o 3 className predefinidas que uses en todo tu sitio web y solo pasar 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.
Abrazo. Bliss.
© 2016 - 2023 Fixtergeek