Blog


Crear componentes de React reutilizables y escalables

CREAR COMPONENTES DE REACT REUTILIZABLES Y ESCALABLES

23 / 05 / 2023 Otros

React es una de las bibliotecas de JavaScript más populares y utilizadas en la actualidad. Una de las grandes ventajas de React es su capacidad para crear componentes reutilizables. En este artículo, vamos a hablar sobre cómo crear componentes reutilizables en React, incluyendo patrones comunes de diseño de componentes, cómo utilizar las props y los children para crear componentes flexibles y cómo construir una biblioteca de componentes.

PATRONES COMUNES DE DISEÑO DE COMPONENTES

Antes de comenzar a hablar sobre cómo crear componentes reutilizables, es importante comprender algunos patrones comunes de diseño de componentes en React. Estos patrones son útiles para crear componentes flexibles y reutilizables.

Componentes de presentación y componentes de contenedor

Una de las formas más comunes de diseñar componentes en React es separarlos en componentes de presentación y componentes de contenedor. Los componentes de presentación (también conocidos como "dumb components" o "stateless components") son aquellos que se encargan exclusivamente de renderizar elementos visuales, mientras que los componentes de contenedor (también conocidos como "smart components" o "stateful components") se encargan de manejar el estado y la lógica del negocio.

import React from 'react';

interface Props {
  // Define las props que necesita tu componente
  imageUrl: string;
  title: string;
  description: string;
}

const Card = (props: Props) => {
  // Renderiza el componente
  return (
    <div>
      <img src={ props.imageUrl } alt={ props.title } />
      <h2>{ props.title }</h2>
      <p>{ props.description }</p>
    </div>
  );
}

export default Card;

En este ejemplo, se define un componente de presentación Card que recibe props para la imagen, el título y la descripción. El componente se encarga exclusivamente de renderizar estos elementos visuales.

import React, { useState } from 'react';
import Card from './Card';

interface Props {
  // Define las props que necesita tu componente
  cardData: {
    imageUrl: string;
    title: string;
    description: string;
  }[];
}

const CardList = (props: Props) => {
  const [selectedCard, setSelectedCard] = useState(0);

  const handleCardClick = (index: number) => {
    setSelectedCard(index);
  }

  // Renderiza los componentes de presentación y maneja el estado y la lógica del negocio
  return (
    <div>
      {props.cardData.map((card, index) => (
        <Card
          key={ index }
          imageUrl={ card.imageUrl }
          title={ card.title }
          description={ card.description }
          onClick={() => handleCardClick(index)}
        />
      ))}
      <p>La tarjeta seleccionada es: { props.cardData[selectedCard].title }</p>
    </div>
  );
}

export default CardList;

En este ejemplo, se define un componente de contenedor CardList que recibe un array de datos para crear múltiples Card componentes. El componente maneja el estado y la lógica del negocio (en este caso, seleccionar una tarjeta cuando se hace clic en ella) y se encarga de pasar las props necesarias a cada componente de presentación Card.

Componentes de orden superior (HOC)

Los componentes de orden superior (HOC, por sus siglas en inglés) son una técnica de patrón de diseño que permite envolver un componente con lógica adicional, es decir, toma un componente y devuelve otro componente con funcionalidad adicional. Esto es útil cuando se desea agregar comportamiento adicional a un componente sin modificar el propio componente.

import React, { ComponentType } from 'react';

interface Props {
  // Define las props que necesita tu componente
  title: string;
}

const withTitle<T extends Props> = (
  WrappedComponent: ComponentType<T>,
  title: string
) => {
  // Devuelve un nuevo componente que envuelve al componente original
  return function WithTitle(props: T) {
    return (
      <>
        <h1>{title}</h1>
        <WrappedComponent {...props} />
      </>
    );
  };
}

export default withTitle;

En este ejemplo, se define una función withTitle que toma un componente WrappedComponent y un título como argumentos y devuelve un nuevo componente que envuelve al componente original y agrega el título como encabezado. El nuevo componente se puede utilizar en lugar del componente original en cualquier lugar donde se necesite un encabezado.

import React from 'react';
import withTitle from './withTitle';

interface Props {
  // Define las props que necesita tu componente
  name: string;
}

const Greeting = (props: Props) => {
  // Renderiza el componente
  return <p>¡Hola, { props.name }!</p>;
}

export default withTitle(Greeting, 'Mi aplicación');

En este ejemplo, se define un componente Greeting que recibe un nombre y devuelve un saludo. Para agregar un encabezado a este componente, se utiliza la función withTitle para crear un nuevo componente que envuelve al componente original Greeting y agrega el título "Mi aplicación" como encabezado.

Render Props

La técnica de Render Props se utiliza cuando se desea proporcionar un componente con la capacidad de renderizar otro componente. Esto permite crear componentes altamente reutilizables y flexibles.

UTILIZACIÓN DE PROPS Y CHILDREN PARA CREAR COMPONENTES FLEXIBLES

Una de las formas más comunes de crear componentes reutilizables en React es mediante el uso de props y children. Las props son los parámetros que se pasan a un componente y los children son el contenido que se incluye dentro de un componente.

Props

Las props se utilizan para pasar datos a un componente. Por ejemplo, si se tiene un componente que muestra una imagen, se podría pasar la URL de la imagen como una prop. De esta forma, el componente podría ser utilizado para mostrar diferentes imágenes simplemente pasando una prop diferente cada vez que se utiliza.

import React, { ReactNode } from 'react';

interface Props {
  // Define las props que necesita tu componente
  render: (data: { title: string; content: ReactNode }) => ReactNode;
}

const BlogPost = (props: Props) => {
  const data = { title: 'Mi primer post', content: <p>¡Hola, mundo!</p> };

  // Renderiza el componente y pasa los datos al render prop
  return <div>{ props.render(data) }</div>;
}

export default BlogPost;

En este ejemplo, se define un componente BlogPost que recibe una función render como prop. El componente luego crea un objeto de datos y pasa este objeto como argumento a la función render. La función render es responsable de renderizar el contenido adicional que se agregará al componente.

import React from 'react';
import BlogPost from './BlogPost';

function App() {
  // Renderiza el componente y define la función render
  return (
    <BlogPost
      render={(data) => (
        <>
          <h1>{ data.title }</h1>
          { data.content }
        </>
      )}
    />
  );
}

export default App;

En este ejemplo, se utiliza el componente BlogPost en una aplicación más grande. La función render se define como una función de flecha en línea que recibe los datos del post y devuelve los elementos visuales que se agregarán al componente.

Children

Los children se utilizan para incluir contenido dentro de un componente. Por ejemplo, si se tiene un componente que muestra un botón, se podría incluir el texto del botón dentro de los children. De esta forma, el botón podría ser utilizado con diferentes textos simplemente cambiando los children cada vez que se utiliza.

Las props y los children se utilizan comúnmente para crear componentes flexibles que se pueden reutilizar en diferentes contextos.

import React, { ReactNode } from 'react';

interface Props {
  // Define las props que necesita tu componente
  header: ReactNode;
  content: ReactNode;
  footer: ReactNode;
}

const Page = (props: Props) => {
  // Renderiza el componente y utiliza las props y los children para crear un layout flexible
  return (
    <div>
      { props.header }
      <main>{ props.content }</main>
      { props.footer }
    </div>
  );
}

export default Page;

En este ejemplo, se define un componente Page que recibe props para el encabezado, el contenido y el pie de página. El componente utiliza las props y los children para crear un layout flexible que puede ser utilizado en diferentes contextos.

import React from 'react';
import Page from './Page';

const App = () => {
  // Renderiza el componente y pasa los elementos visuales como props y children
  return (
    <Page
      header={ <h1>Encabezado</h1> }
      content={ <p>Contenido de la página</p> }
      footer={ <footer>Pie de página</footer> }
    />
  );
}

export default App;   

 

En este ejemplo, se utiliza el componente Page en una aplicación más grande. Los elementos visuales se pasan como props y children, lo que permite una gran flexibilidad en la construcción del layout de la página.

CONSTRUCCIÓN DE UNA BIBLIOTECA DE COMPONENTES

Una vez que se han creado varios componentes reutilizables, se puede comenzar a construir una biblioteca de componentes. La construcción de una biblioteca de componentes puede ser útil para reutilizar componentes en diferentes proyectos.

Componentes atómicos

La construcción de una biblioteca de componentes comienza con la creación de componentes atómicos. Los componentes atómicos son los componentes más simples y básicos que se pueden crear, son componentes muy simples que representan una sola pieza de la interfaz de usuario. Estos componentes pueden ser combinados para crear componentes más complejos.

 

import React from 'react';

interface Props {
  // Define las props que necesita tu componente
  label: string;
  onClick: () => void;
}

const Button = (props: Props) => {
  // Renderiza el componente
  return <button onClick={ props.onClick }>{ props.label }</button>;
}

export default Button;

 

En este ejemplo, se define un componente Button que recibe una etiqueta y una función onClick como props. El componente devuelve un botón con la etiqueta y el evento onClick asociado.

Diseño de componentes modulares

Cuando se construye una biblioteca de componentes, es importante diseñar los componentes de forma modular. Esto significa que cada componente debe ser independiente y no depender de otros componentes para funcionar correctamente. De esta forma, los componentes pueden ser reutilizados en diferentes proyectos sin necesidad de hacer grandes modificaciones. Un enfoque común es utilizar componentes atómicos para construir componentes más complejos.

import React from 'react';
import Button from './Button';

interface Props {
  // Define las props que necesita tu componente
  label: string;
  onClick: () => void;
}

const SubmitButton = (props: Props) => {
  // Renderiza el componente utilizando el componente atómico Button
  return <Button label={ props.label } onClick={ props.onClick } />;
}

export default SubmitButton;

En este ejemplo, se define un componente SubmitButton que utiliza el componente atómico Button para renderizar un botón de envío. Este enfoque permite una gran flexibilidad en la construcción de interfaces de usuario complejas.

Documentación

Para que una biblioteca de componentes sea útil, es importante incluir una documentación clara y detallada. La documentación debe incluir una descripción de cada componente, cómo utilizarlo, qué props y children están disponibles y ejemplos de uso. De esta forma, los usuarios pueden utilizar la biblioteca de componentes de forma efectiva.

import React from 'react';

/**
 * Un componente de botón reutilizable.
 *
 * @param {string} label - La etiqueta del botón.
 * @param {function} onClick - La función que se llamará cuando se haga clic en el botón.
 * @returns {JSX.Element}
 */
const Button = ({ label, onClick }) => {
  return <button onClick={ onClick }>{ label }</button>;
}

export default Button;

En este ejemplo, se utilizan comentarios de JSDoc para documentar el componente Button. Los comentarios explican los props que recibe el componente (label y onClick) y qué devuelve (JSX.Element). Esta documentación puede ser muy útil para otros desarrolladores que están trabajando con el componente.

Pruebas unitarias

Finalmente, es importante incluir pruebas unitarias para cada componente en la biblioteca. Las pruebas unitarias aseguran que cada componente funcione correctamente y ayuda a prevenir errores en el futuro. Además, las pruebas unitarias hacen que sea más fácil mantener y actualizar la biblioteca de componentes con el tiempo.

 

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Button from './Button';

test('renderiza un botón con la etiqueta correcta', () => {
  const label = 'Haz clic aquí';
  const onClick = jest.fn();
  const { getByText } = render(<Button label={ label } onClick={ onClick } />);
  const button = getByText(label);
  expect(button).toBeInTheDocument();
});

test('llama a la función onClick cuando se hace clic en el botón', () => {
  const label = 'Haz clic aquí';
  const onClick = jest.fn();
  const { getByText } = render(<Button label={ label } onClick={ onClick } />);
  const button = getByText(label);
  fireEvent.click(button);
  expect(onClick).toHaveBeenCalled();
});

En la primera prueba, se verifica que el botón se renderiza correctamente con la etiqueta correcta utilizando la función getByText() de @testing-library/react.

En la segunda prueba, se verifica que la función onClick se llama correctamente cuando se hace clic en el botón utilizando la función fireEvent.click() de @testing-library/react.

Estas pruebas aseguran que el componente funciona correctamente y permite detectar errores y problemas de forma temprana.

CONCLUSIONES

Crear componentes reutilizables en React es esencial para escribir código limpio y mantenible. Los patrones comunes de diseño de componentes, como los componentes de presentación y de contenedor, los componentes de orden superior y los Render Props, son útiles para crear componentes flexibles y reutilizables. El uso de props y children también es fundamental para crear componentes que pueden ser utilizados en diferentes situaciones. Finalmente, la construcción de una biblioteca de componentes puede ayudar a reutilizar los componentes en diferentes proyectos, y debe incluir una documentación clara, pruebas unitarias y un diseño modular.



ARTÍCULOS RELACIONADOS