Estructurando aplicaciones React de forma escalable

Estructurando aplicaciones React de forma escalable

React se ha convertido en una de las bibliotecas de JavaScript más populares para crear interfaces de usuario increíbles. Su flexibilidad y eficiencia permitieron el desarrollo de aplicaciones web modernas y dinámicas. Sin embargo, a medida que los proyectos de React crecen en escala y complejidad, surge la necesidad de adoptar una estructura sólida que promueva la mantenibilidad, la reutilización del código y, por supuesto, la escalabilidad. En este artículo, cubriremos algunas prácticas para estructurar aplicaciones React de manera escalable, destacando la importancia de la organización del código.

Pero diré que lo que voy a explicar aquí no es la regla definitiva. Ciertamente hay muchas formas de estructurar las aplicaciones. Sin embargo, quiero exponer mi punto de vista sobre este tema, ya que no veo que se hable mucho de ello. Para hacer esto, cubriré 3 puntos principales de las aplicaciones React: componentes, hooks y contexto.

Definición previa de términos técnicos

Antes de explorar prácticas para estructurar aplicaciones React de forma escalable, es útil establecer una definición preliminar de algunos términos técnicos de uso frecuente. Esto nos ayudará a garantizar que todos los lectores, independientemente de su nivel de conocimiento, estén familiarizados con los conceptos tratados a lo largo del texto. Vamos allá:

Componentes: En el contexto de React, un componente es una unidad reutilizable y autónoma que encapsula la lógica y la representación de la interfaz de usuario. Los componentes se construyen a partir de elementos de React y se pueden componer entre sí para crear jerarquías complejas. Este enfoque modular facilita el mantenimiento, las pruebas y la reutilización del código.

Hooks: Los hooks son funciones especiales proporcionadas por React que le permiten usar características de React, como el estado y el ciclo de vida, en componentes funcionales. Permiten que los componentes funcionales tengan un estado interno y accedan a los recursos de React sin la necesidad de convertir los componentes en clases. Con los hooks, es posible agregar comportamientos y funcionalidades a los componentes de una manera más sencilla y legible.

Contexto: El contexto es una característica de React que permite compartir datos entre componentes sin tener que pasarlos explícitamente a través de propiedades. Es especialmente útil cuando tenemos varios componentes anidados que necesitan acceder a los mismos datos. El contexto crea un "canal" de comunicación global, que permite a los componentes consumir y actualizar los datos proporcionados por el proveedor de contexto.

Ahora que hemos establecido las definiciones de los términos técnicos fundamentales, estamos listos para seguir adelante.

La importancia de la Estructuración Escalable

La correcta organización del código en una aplicación React juega un papel fundamental en el éxito del proyecto. Una buena estructura no sólo hace que la aplicación sea más fácil de entender y mantener, sino que también facilita su expansión con el tiempo. Además, una buena estructuración promueve la reutilización del código, reduciendo la duplicación de esfuerzos y acelerando el proceso de desarrollo. Invertir en estructuración escalable desde el principio es esencial para evitar problemas futuros y garantizar un desarrollo fluido, proporcionando una base sólida para el crecimiento y la mejora continua de la aplicación.

Para comprender mejor la importancia de lo que explicaremos más adelante, consideremos un ejemplo de una aplicación mal estructurada. En este ejemplo ficticio, tenemos un mini comercio electrónico cuya complejidad aumenta a medida que se agregan más funciones. Sin embargo, la estructura del código no sigue el ritmo de este crecimiento, lo que resulta en un código desorganizado y difícil de mantener.

En el siguiente código, podemos ver una implementación inicial de la aplicación:

import React, { useState, useEffect } from "react";

export const App = () => {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);
  const [searchTerm, setSearchTerm] = useState("");

  useEffect(() => {
    fetchProducts();
  }, []);

  const fetchProducts = async () => {
    const response = await fetch("https://dummyjson.com/products/?limit=10");
    const { products } = await response.json();
    setProducts(products);
  };

  const addToCart = (product) => {
    setCart([...cart, product]);
  };

  const removeFromCart = (item) => {
    const updatedCart = cart.filter((cartItem) => cartItem.id !== item.id);
    setCart(updatedCart);
  };

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
  };

  const filteredProducts = products.filter((product) =>
    product.title.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <div>
      <h1>Mini Comércio Eletrônico</h1>
      <div>
        <h2>Produtos</h2>
        <input
          type="text"
          placeholder="Pesquisar produtos"
          value={searchTerm}
          onChange={handleSearch}
        />
        <ul>
          {filteredProducts.map((product) => (
            <li key={product.id}>
              {product.title} - R$ {product.price.toFixed(2)}
              <button onClick={() => addToCart(product)}>
                Adicionar ao Carrinho
              </button>
            </li>
          ))}
        </ul>
      </div>
      <div>
        <h2>Carrinho</h2>
        <ul>
          {cart.map((item) => (
            <li key={item.id}>
              {item.title} - R$ {item.price.toFixed(2)}
              <button onClick={() => removeFromCart(item)}>
                Remover do Carrinho
              </button>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};


En este ejemplo, todas las responsabilidades de la aplicación están en el componente App. Esto incluye el estado del producto, el carrito y el término de búsqueda, agregar y eliminar artículos al carrito, así como manejar la búsqueda de productos. Además, muestra las listas de productos y carritos. A medida que la aplicación crece, esta estructura monolítica se vuelve insostenible, lo que dificulta comprender, mantener y probar el código. Entonces, ¿cómo podemos mejorar esto?

1. Componentes como Unidades Independientes

Una práctica fundamental para estructurar aplicaciones React es crear componentes como unidades independientes. Esto significa que cada componente debe ser responsable de una única funcionalidad o parte de la interfaz de usuario. Dividiendo la interfaz en componentes más pequeños y específicos, es posible obtener una estructura modular y reutilizable.

En el ejemplo anterior, podemos reestructurar el código en componentes independientes, como ProductList y Cart, para mejorar la organización y reutilización del código.

import React, { useState, useEffect } from "react";

const ProductList = ({ products, addToCart }) => {
  const [searchTerm, setSearchTerm] = useState("");

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
  };

  const filteredProducts = products.filter((product) =>
    product.title.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <div>
      <h2>Produtos</h2>
      <input
        type="text"
        placeholder="Pesquisar produtos"
        value={searchTerm}
        onChange={handleSearch}
      />
      <ul>
        {filteredProducts.map((product) => (
          <li key={product.id}>
            {product.title} - R$ {product.price.toFixed(2)}
            <button onClick={() => addToCart(product)}>
              Adicionar ao Carrinho
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

const Cart = ({ cart, removeFromCart }) => {
  return (
    <div>
      <h2>Carrinho</h2>
      <ul>
        {cart.map((item) => (
          <li key={item.id}>
            {item.title} - R$ {item.price.toFixed(2)}
            <button onClick={() => removeFromCart(item)}>
              Remover do Carrinho
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export const App = () => {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);

  useEffect(() => {
    fetchProducts();
  }, []);

  const fetchProducts = async () => {
    const response = await fetch("<https://dummyjson.com/products/?limit=10>");
    const { products } = await response.json();
    setProducts(products);
  };

  const addToCart = (product) => {
    setCart([...cart, product]);
  };

  const removeFromCart = (item) => {
    const updatedCart = cart.filter((cartItem) => cartItem.id !== item.id);
    setCart(updatedCart);
  };

  return (
    <div>
      <h1>Mini Comércio Eletrônico</h1>
      <ProductList products={products} addToCart={addToCart} />
      <Cart cart={cart} removeFromCart={removeFromCart} />
    </div>
  );
};


En el ejemplo actualizado, el componente ProductList gestiona la visualización de productos, incluida la función de búsqueda y la adición de artículos al carrito. El componente Cart es responsable de mostrar los artículos en el carrito y retirarlos.

Este enfoque de componentes independientes facilita el mantenimiento, prueba y reutilización del código, ya que ahora el listado de productos y el carrito se pueden incluir en diferentes partes de la aplicación, sin que necesariamente estén juntos. Además de permitir el uso de hooks de React para agregar lógica y efectos específicos a cada componente.

2. Hooks personalizados para la lógica del Negocio

Los hooks personalizados son una herramienta poderosa para organizar y reutilizar la lógica empresarial en aplicaciones React. Facilitan tratar la lógica compleja por separado y promueven la reutilización del código, permitiendo compartir la lógica empresarial entre diferentes componentes de una forma sencilla.

En el ejemplo anterior, podríamos crear un hook personalizado para lidiar con la lógica de filtrar los productos:

const useProductFilter = (products) => {
  const [searchTerm, setSearchTerm] = useState('');

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
  };

  const filteredProducts = products.filter((product) =>
    product.title.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return [searchTerm, handleSearch, filteredProducts];
};


Con este hook personalizado, podemos simplificar el componente ProductList:

const ProductList = ({ products, addToCart }) => {
  const [searchTerm, handleSearch, filteredProducts] =
    useProductFilter(products);

  return (
    <div>
      <h2>Produtos</h2>
      <input
        type="text"
        placeholder="Pesquisar produtos"
        value={searchTerm}
        onChange={handleSearch}
      />
      <ul>
        {filteredProducts.map((product) => (
          <li key={product.id}>
            {product.title} - R$ {product.price.toFixed(2)}
            <button onClick={() => addToCart(product)}>
              Adicionar ao Carrinho
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};


Este abordaje al usar hooks personalizados ayuda a mantener los componentes más limpios y centrados en su responsabilidad principal, mientras que la lógica compleja se maneja por separado. Los hooks también promueven la reutilización de código, permitiendo compartir la lógica empresarial entre diferentes componentes de forma sencilla.

3. Manejo de Estado Global con Contexto

La gestión del estado es una consideración importante cuando se trata de aplicaciones React de tamaño mediano a grande. Una de las formas de abordar el estado global es utilizar el contexto de React.

Continuando con nuestro ejemplo, podemos usar el contexto para compartir el estado de los productos y el carrito entre componentes, eliminando la necesidad de pasar accesorios a través de múltiples niveles del árbol de componentes.

import { createContext, useEffect } from "react";

export const ProductsContext = createContext();

export const ProductsProvider = ({ children }) => {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);

  useEffect(() => {
    fetchProducts();
  }, []);

  const fetchProducts = async () => {
    const response = await fetch("<https://dummyjson.com/products/?limit=10>");
    const { products } = await response.json();
    setProducts(products);
  };

  const addToCart = (product) => {
    setCart([...cart, product]);
  };

  const removeFromCart = (item) => {
    const updatedCart = cart.filter((cartItem) => cartItem.id !== item.id);
    setCart(updatedCart);
  };

  return (
    <ProductsContext.Provider
      value={{
        products,
        cart,
        addToCart,
        removeFromCart,
      }}
    >
      {children}
    </ProductsContext.Provider>
  );
};


Aquí creamos un contexto ProductsContext y el proveedor ProductsProvider, que involucra los componentes de la aplicación que necesitan acceder al contexto. Contiene los estados de los productos y carritos, y las funciones para manipular los estados. De esta manera, cualquier componente descendiente de estos componentes puede acceder al estado y las funciones proporcionadas por el contexto.

const ProductList = () => {
  const { products, addToCart } = useContext(ProductsContext);
  const [searchTerm, handleSearch, filteredProducts] =
    useProductFilter(products);

  // resto do código...
};

const Cart = () => {
  const { cart, removeFromCart } = useContext(ProductsContext);
  // resto do código...
};

export const App = () => {
  return (
    <ProductsProvider>
      <h1>Mini Comércio Eletrônico</h1>
      <ProductList />
      <Cart />
    </ProductsProvider>
  );
};


En estos componentes utilizamos la función useContext para acceder a la información y funciones de ProductsContext. De esta manera, ya no necesitamos pasar accesorios manualmente entre componentes. De hecho, si siguiéramos con este ejemplo y fuera necesario aumentar funcionalidad, se podría dividir en dos contextos, uno para los productos y otro para el carrito, lo que separaría aún más responsabilidades.

El uso del contexto simplifica la gestión del estado global y evita la propagación innecesaria de accesorios en una aplicación a gran escala. Además, termina haciendo que la capacidad de prueba del código sea mucho más fácil, ya que es posible hacer un mock de valores y funciones simplemente pasando un nuevo proveedor con los valores y así probando los componentes que desees. Sin embargo, es importante tener cuidado al utilizar el contexto en exceso, ya que esto puede hacer que el código sea más complejo y difícil de entender. Utilice el contexto de forma selectiva y solo para datos y funciones realmente compartidos por varios componentes.

Conclusión

Estructurar las aplicaciones React de forma escalable es esencial para garantizar una base sólida para el crecimiento y el mantenimiento del código. Al dividir la interfaz en componentes independientes, utilizar enlaces personalizados para la lógica empresarial y adoptar el contexto para la gestión del estado global, podemos obtener una estructura más organizada, modular, reutilizable y más fácilmente comprobable.

Pero es importante enfatizar nuevamente que no existe una regla definitiva para estructurar las aplicaciones React y las prácticas presentadas son solo sugerencias. Cada proyecto puede tener sus particularidades y requerir enfoques específicos. Por ejemplo, en muchos proyectos utilizar el contexto puede no ser la mejor alternativa, debido a la complejidad de los estados. Entonces, lo más importante es buscar formas que contribuyan a la legibilidad, mantenimiento y escalabilidad de las aplicaciones, permitiendo un desarrollo más eficiente y sin problemas en el futuro.

En cualquier caso, ¡felicidades por llegar hasta aquí! Espero haber contribuido eficazmente con tus próximos proyectos.

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.