Inicio / Artículos

Mantener los ganchos sin cabeza: separar el comportamiento de las alertas del renderizado de las mismas

Aprenda por qué un hook de React que devuelve JSX oculta parte del árbol de componentes, vincula el comportamiento con la presentación, y cómo refactorizarlo para exponer el estado en su lugar.

Editor: REACTAPP.TOP5 min de lectura1092 palabras

Fuentes: Why I’m Suspicious of Hooks That Return JSX

Los ganchos personalizados son excelentes para ocultar los detalles de implementación, pero a veces ocultan demasiado. Un gancho que devuelve JSX ya listo parece conveniente en el lugar donde se llama, pero traslada parte del árbol de la interfaz de usuario detrás de una API que parece contener datos simples, y une el comportamiento a una presentación específica. Utilizando un gancho típico de alertas de un editor de paneles, este artículo muestra cómo reconocer este problema, cómo refactorizarlo hacia un gancho sin interfaz gráfica y qué se gana con esas pocas líneas adicionales.

Un gancho de alertas que renderiza su propia interfaz

Las alertas son ideales para un gancho: necesitan estado, posiblemente temporizadores, manejadores de cierre y algún tipo de cola. Así es como se utiliza dicho gancho:

const {
  showSuccess,
  showError,
  alerts,
} = useAlerts();

return (
  <>
    {alerts}

    <Dashboard />
  </>
);

Ya hay una pista de que algo no está bien. alerts se incluye directamente en el JSX, lo cual sugiere fuertemente que el hook devuelve elementos renderizados. El hook es responsable del comportamiento, ya que proporciona showSuccess y showError, y aparentemente también de cómo se ven las alertas.

La implementación lo confirma:

const useAlerts = () => {
  const [alerts, setAlerts] = useState<Alert[]>([]);

  const showSuccess = (message: string) => {
    // update state
  };

  const showError = (message: string) => {
    // update state
  };

  ...

  return {
    showSuccess,
    showError,
    alerts: (
      <AlertContainer>
        {alerts.map(({ id, type, message }) => (
          <Alert
            key={id}
            type={type}
            message={message}
          />
        ))}
      </AlertContainer>
    ),
  };
};

useAlerts controla el estado de las alertas, las funciones que lo modifican y la marcaje: el contenedor AlertContainer y una Alert por entrada. La separación entre lógica y presentación ha desaparecido.

Por qué el JSX devuelto es engañoso

Parece ser datos

Desde el punto de vista del consumidor, nada indica que alerts sea un subárbol de React. Se encuentra junto a dos funciones, tiene un nombre plural que sugiere un array de registros, y podría confundirse fácilmente con el estado:

const {
  showSuccess,
  showError,
  alerts,
} = useAlerts();

La única forma de conocer la verdad es abrir el hook.

Esconde de dónde proviene la interfaz de usuario

Un árbol de componentes es valioso en parte porque documenta de dónde proviene cada elemento de la interfaz. Al revisar el panel de control y ver esto:

{alerts}

<Dashboard />

no se puede saber qué renderiza alerts, qué componentes monta, de qué contexto o propiedades dependen esos componentes, ni cómo se estilan, sin adentrarse en el hook. Parte del árbol ha sido reubicada detrás de algo que parece tener un carácter comportamental.

Vincula dos decisiones no relacionadas

El hook ya no puede exponer el comportamiento de las alertas sin decidir también el componente correspondiente. Cualquier pantalla que necesite la lógica de alertas hereda su apariencia, independientemente de si encaja o no. No existe razón técnica para que estas dos cuestiones vayan juntas.

Refactorización hacia un hook sin interfaz gráfica

El hook debe gestionar el estado y las operaciones sobre él, y nada más. Se debe exponer la lista raw de alertas junto con las acciones, incluyendo una forma de cerrar una alerta:

const {
  alerts,
  showSuccess,
  showError,
  dismissAlert,
} = useAlerts();

Luego, se renderizan las alertas en el componente, donde ya corresponde hacerlo:

<AlertContainer>
  {alerts.map(({ id, type, message }) => (
    <Alert
      key={id}
      type={type}
      message={message}
      onDismiss={() => dismissAlert(id)}
    />
  ))}
</AlertContainer>

<Dashboard />

Ahora el árbol de componentes vuelve a ser explícito. Quien lo lea verá un AlertContainer, una Alert por entrada identificada por id, y un manejador para cerrar las alertas conectado al hook. El papel del hook es claro: suministra el estado y las acciones.

¿Es un problema el código adicional?

El componente que consume datos sí se vuelve más largo, y eso está bien. Es tentador pulir una API hasta que quien la llama apenas escriba algo, pero ese perfeccionismo puede eliminar información arquitectónica útil. Cinco líneas de código con estructura clara son más fáciles de mantener que un único {alerts} que oculta toda una parte de la interfaz de usuario.

Si varias pantallas muestran alertas de manera idéntica, aún se puede evitar la repetición sin volver a integrar la interfaz en el hook: crea un componente AlertList que reciba alerts y onDismiss como propiedades, o uno que llame al hook directamente. De cualquier manera, la renderización ocurre en un componente, el cual es visible en el árbol de componentes y puede ser analizado, memorizado y probado como cualquier otro componente.

Se vuelve posible reutilizar

Al tener JSX integrado, reutilizar el comportamiento obliga a reutilizar también la presentación, una suposición que no siempre es válida. Con la versión sin interfaz gráfica, un área de notificaciones, un banner en línea y un diseño para móviles pueden compartir exactamente el mismo ciclo de vida de las alertas al renderizarlas de manera diferente. El componente de alertas también puede evolucionar por sí mismo sin afectar al gancho que gestiona el temporizador y la eliminación de las mismas.

Una trampa relacionada: ganchos que devuelven componentes

Un caso similar a este problema es cuando un gancho define y devuelve una función de componente, como const AlertView = () => ... dentro del cuerpo del gancho. Dado que se crea una nueva función en cada renderizado, React considera un nuevo tipo de componente cada vez y desmonta y vuelve a montar ese subárbol, borrando su estado interno y el DOM. Devolver elementos, como lo hace el gancho original, evita ese error específico, pero el problema de diseño descrito anteriormente persiste.

Puntos clave

  • El uso de JSX proveniente de un hook generalmente significa que dos responsabilidades se han fusionado en una sola abstracción.
  • Un valor devuelto que parece ser datos pero en realidad corresponde a la interfaz de usuario oculta parte del árbol de componentes para los lectores.
  • Mantenga los hooks sin cabeza: devuelva el estado y las acciones, y renderice en los componentes.
  • Para una presentación compartida, extraiga un componente en lugar de devolver marcado desde el hook.
  • Al revisar un hook de este tipo, haga una pregunta: ¿qué razón tiene este hook para decidir cómo se mostrará la interfaz de usuario?

Lecturas relacionadas

  • Componentes de Orden Mayor desde Envoltorios de Clase hasta Funciones y Gancho — Aprenda cómo funcionan los componentes de orden mayor en React, cómo se presenta el mismo HOC como clase y como función, y por qué los gancho personalizados reemplazaron la mayoría de los casos de uso de HOC.
  • Prompts Limitados: Impidiendo que los Asistentes de IA Inventen un Stack Paralelo — Por qué las solicitudes de funcionalidades vagas hacen que los asistentes de programación añadan bibliotecas y envoltorios duplicados, y cómo un prompt limitado con rutas, invariantes y límites estrictos soluciona este problema.