SOLID + ReactJS
Tags: react, solid
Published: 2024-04-27 22:28:01

Los principios **SOLID** son una parte fundamental del desarrollo de software y son esenciales para crear aplicaciones sólidas y escalables. Fueron introducidos por el ingeniero de software Robert C. Martin, y cada uno de ellos aborda un aspecto específico del diseño de software. En el contexto de ReactJS, aplicar estos principios puede ayudar a crear una arquitectura más cohesiva y modular, lo que puede resultar en un código más fácil de mantener y de modificar.

En este artículo, vamos a explorar los cinco principios **SOLID** y cómo se aplican específicamente en ReactJS. 
Cubriremos ejemplos prácticos de cómo aplicar cada principio y discutiremos por qué son importantes para el desarrollo de aplicaciones de calidad. Ya seas un desarrollador principiante o experimentado, esperamos que este artículo te brinde información útil sobre cómo aplicar los principios SOLID en ReactJS. ¡Comencemos!

- **Responsabilidad Única (Single Responsibility Principle):** Este principio establece que una clase o componente debe tener una única responsabilidad. Esto significa que una clase o componente debe tener una sola razón para cambiar. Al aplicar este principio, se puede mejorar la modularidad de una aplicación y hacer que el código sea más fácil de mantener y de entender. En ReactJS, esto se traduce en asegurarse de que cada componente tenga una función única y no se responsabilice de demasiadas tareas. Por ejemplo, dividir un componente de lista de tareas en componentes más pequeños que se encarguen de mostrar cada tarea individualmente.

Imaginemos que tenemos un componente llamado TodoList que muestra una lista de tareas. El componente TodoList también se encarga de manejar la lógica para agregar nuevas tareas y eliminar tareas existentes. Sin embargo, esto viola el principio de Responsabilidad Única, ya que el componente está manejando múltiples responsabilidades.

Para aplicar el principio de Responsabilidad Única, podemos dividir el componente TodoList en componentes más pequeños y especializados. En lugar de manejar toda la lógica de la lista de tareas en un solo componente, podemos crear componentes más pequeños y especializados para manejar tareas individuales y para agregar y eliminar tareas.

A continuación, te mostraré un ejemplo de código donde hemos aplicado este principio. Hemos dividido el componente TodoList en componentes más pequeños y especializados que se encargan de tareas individuales y la lógica de agregar y eliminar tareas:

```javascript
function TodoItem({ task, onRemove }) {
  return (
    <li>
      {task}
      <button onClick={onRemove}>Eliminar tarea</button>
    </li>
  );
}
```

```javascript
function useTodoList() {
  const [tasks, setTasks] = useState([]);

  function handleAdd(newTask) {
    if (newTask !== "") {
      setTasks([...tasks, newTask]);
    }
  }

  function handleRemove(index) {
    const newTasks = [...tasks];
    newTasks.splice(index, 1);
    setTasks(newTasks);
  }

  return [tasks, handleAdd, handleRemove];
}
```

```javascript
function TodoList() {
  const [tasks, handleAdd, handleRemove] = useTodoList();
  const [newTask, setNewTask] = useState("");

  function handleChange(e) {
    setNewTask(e.target.value);
  }

  function handleAddTask() {
    handleAdd(newTask);
    setNewTask("");
  }

  return (
    <div>
      <ul>
        {tasks.map((task, index) => (
          <TodoItem
            key={index}
            task={task}
            onRemove={() => handleRemove(index)}
          />
        ))}
      </ul>
      <div>
        <input value={newTask} onChange={handleChange} />
        <button onClick={handleAddTask}>Agregar tarea</button>
      </div>
    </div>
  );
}
```

En este ejemplo, hemos creado un hook personalizado **useTodoList** que se encarga de manejar la lista de tareas y la lógica para agregar y eliminar tareas. Luego, el componente TodoList utiliza el hook para obtener la lista de tareas y los manejadores handleAdd y handleRemove.

El componente TodoList solo se encarga de renderizar la lista de tareas y manejar las interacciones del usuario, lo que cumple con el principio de Responsabilidad Única.

Al separar la lógica del componente TodoList en un hook personalizado, hemos hecho que el código sea aún más modular y fácil de entender y de mantener. Además, hemos mejorado la reutilización del código, ya que ahora podemos usar el hook useTodoList en cualquier componente que necesite manejar una lista de tareas.

- **Abierto/Cerrado (Open/Closed Principle):** Este principio establece que una clase o componente debe estar cerrado a la modificación pero abierto a la extensión. Esto significa que una clase o componente debe ser fácilmente extensible sin tener que modificar su código existente. En ReactJS, esto significa que deberíamos poder agregar nuevas características a nuestro código sin tener que modificar el código existente. 

Para aplicar este principio en React, podemos crear componentes reutilizables que sean fácilmente extensibles sin tener que modificar su código original.

Por ejemplo, supongamos que tenemos un componente Button que se utiliza en varios lugares de nuestra aplicación. Ahora necesitamos agregar una nueva funcionalidad a este componente: agregar un icono junto al texto del botón. En lugar de modificar directamente el código del componente Button, podemos crear un nuevo componente que extienda la funcionalidad del Button original.

Aquí te muestro cómo quedaría el código:

```javascript
import React from "react";

function Button({ children, ...rest }) {
  return <button {...rest}>{children}</button>;
}

function IconButton({ icon, children, ...rest }) {
  return (
    <Button {...rest}>
      <span>{icon}</span>
      {children}
    </Button>
  );
}

export { Button, IconButton };
```

- **Sustitución de Liskov (Liskov Substitution Principle):** Este principio establece que las clases o componentes derivados deben poder ser utilizados como instancias de sus clases o componentes base sin requerir ninguna modificación adicional. En otras palabras, cualquier instancia de una clase o componente debe poder ser reemplazada por cualquier instancia de su clase o componente base sin que la aplicación falle. En ReactJS, esto significa que los componentes derivados deben poder ser utilizados en lugar de sus componentes base sin causar problemas. Esto se logra manteniendo una estructura coherente y consistente en todos los componentes.

En cuanto al tercer principio, el principio de Sustitución de Liskov, podemos aplicarlo en React cuando utilizamos los patrones de composición y herencia de componentes. En términos generales, podemos decir que un componente hijo debe poder ser utilizado en lugar de su componente padre sin que se produzca un comportamiento incorrecto en la aplicación.

Un ejemplo sencillo de cómo aplicar este principio en React sería el siguiente:

Supongamos que tenemos un componente llamado Button que se utiliza para renderizar botones en nuestra aplicación. Ahora queremos crear un nuevo tipo de botón, llamado SubmitButton, que tenga un estilo diferente y que sea utilizado para enviar formularios.

Podemos crear el nuevo componente SubmitButton heredando de Button y sobrescribiendo el estilo por defecto:

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

function SubmitButton(props) {
  return (
    <Button
      {...props}
      style={{ backgroundColor: 'green', color: 'white' }}
    />
  );
}

export default SubmitButton;
```
En este ejemplo, el componente SubmitButton hereda de Button y sobrescribe el estilo por defecto para tener un fondo verde y un texto en blanco. Podemos utilizar este nuevo componente SubmitButton en lugar del componente Button en cualquier parte de nuestra aplicación donde necesitemos un botón de envío.

De esta manera, hemos aplicado el principio de Sustitución de Liskov en nuestro código de React, ya que el componente SubmitButton se comporta como un tipo de botón válido en cualquier lugar donde se necesite un botón, y podemos utilizarlo sin preocuparnos de que el comportamiento de nuestra aplicación cambie de manera inesperada.

- **Segregación de Interfaces (Interface Segregation Principle):** Este principio establece que una clase o componente no debe implementar interfaces o comportamientos que no necesite. En ReactJS, esto significa que deberíamos dividir nuestros componentes en piezas más pequeñas y especializadas, en lugar de crear un componente gigante que haga todo. Por ejemplo, podemos separar un componente de formulario en componentes más pequeños que se encarguen de campos de entrada individuales, y un componente que se encargue de enviar el formulario.

Supongamos que tenemos un componente Header que muestra el título de una página y tiene un botón de navegación. Podríamos dividir este componente en dos partes: Title y NavigationButton, que manejan específicamente el título y el botón de navegación respectivamente.

```javascript
import React from 'react';

interface HeaderProps {
  title: string;
  onNavigation: () => void;
}

function Header({ title, onNavigation }: HeaderProps) {
  return (
    <div>
      <Title title={title} />
      <NavigationButton onNavigation={onNavigation} />
    </div>
  );
}

interface TitleProps {
  title: string;
}

function Title({ title }: TitleProps) {
  return <h1>{title}</h1>;
}

interface NavigationButtonProps {
  onNavigation: () => void;
}

function NavigationButton({ onNavigation }: NavigationButtonProps) {
  return (
    <button onClick={onNavigation}>
      <span>Go to Next Page</span>
    </button>
  );
}

export default Header;
```

En este ejemplo, Header se ha dividido en dos partes más pequeñas, Title y NavigationButton, que se encargan específicamente de mostrar el título y el botón de navegación, respectivamente. De esta manera, podemos reutilizar estas partes en otros componentes que necesiten mostrar un título o un botón de navegación sin tener que incluir todo el componente Header.

- **Inversión de Dependencias (Dependency Inversion Principle):** Este principio establece que los componentes de nivel superior no deben depender de los componentes de nivel inferior, sino que deben depender de abstracciones. En ReactJS, esto significa que los componentes no deben depender directamente de otros componentes, sino de abstracciones como los props o state. También es importante utilizar patrones de inyección de dependencias para evitar acoplamientos excesivos entre componentes.

Supongamos que tenemos un componente UserList que muestra una lista de usuarios y permite eliminar usuarios haciendo clic en un botón. En lugar de hacer que el componente UserList dependa directamente de una API de usuarios, podemos utilizar una interfaz UserRepository que nos permita acceder a los datos de los usuarios y hacer que la dependencia de UserList sea de la interfaz en lugar de la implementación concreta de la API. De esta manera, podemos cambiar fácilmente la implementación de la interfaz sin tener que cambiar el código del componente UserList.

```javascript
import React, { useState, useEffect } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

interface UserRepository {
  getUsers(): Promise<User[]>;
  deleteUser(id: number): Promise<void>;
}

interface UserListProps {
  userRepository: UserRepository;
}

function UserList({ userRepository }: UserListProps) {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    userRepository.getUsers().then((users) => setUsers(users));
  }, [userRepository]);

  function handleDeleteUser(id: number) {
    userRepository.deleteUser(id).then(() => {
      setUsers(users.filter((user) => user.id !== id));
    });
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <span>{user.name}</span>
          <button onClick={() => handleDeleteUser(user.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

export default UserList;
```

En este ejemplo, el componente UserList utiliza la interfaz UserRepository para acceder a los datos de los usuarios y eliminar usuarios. Ahora podemos proporcionar diferentes implementaciones de UserRepository dependiendo de nuestra necesidad, como una API de usuarios o una simulación de datos en memoria, y el componente UserList funcionará sin cambios en su código.

En resumen, la aplicación de los principios SOLID en React ayuda a crear componentes más cohesivos, flexibles y fáciles de mantener. Cada principio SOLID tiene un propósito específico que ayuda a mejorar la calidad del código y a reducir errores. La correcta aplicación depende del contexto del proyecto, pero su uso puede marcar una gran diferencia en la calidad del software.