Inicio / Artículos

Componentes de orden superior, desde envoltorios de clases hasta funciones y ganchos

Aprenda cómo funcionan los componentes de orden superior en React, cómo se presenta el mismo HOC de registro como clase y como función, y por qué los hooks personalizados reemplazaron la mayoría de los casos de uso de HOC.

Editor: REACTAPP.TOP5 min de lectura1092 palabras

Fuentes: React 27: Higher Order Components (HOC) — Deep Dive (Class vs Functional Components)

Hace mucho tiempo, antes de la existencia de los ganchos personalizados, los desarrolladores de React necesitaban una forma de compartir comportamientos como el registro de datos, las verificaciones de autenticación o la carga de información entre varios componentes. La solución fueron los componentes de orden superior (HOC): una función sencilla que recibe un componente y genera un envoltorio mejorado alrededor de él. A continuación, se muestra cómo el mismo HOC para el registro de datos se implementa tanto como una clase como como una función, y luego se compara con un gancho personalizado.

Por qué aparecieron los HOC en la era de las clases

Cuando la mayor parte del código de React se escribía como clases, reutilizar lógica resultaba complicado. Imagine que Profile, Dashboard y Settings necesitan registrar cuándo se montan y se actualizan, verificar que el usuario está conectado, cargar algunos datos y manejar errores. Sin una abstracción compartida, cada clase repetía el mismo constructor, la configuración del estado, los métodos de ciclo de vida y los métodos auxiliares. Los HOC resolvieron esto al envolver una clase en otra que contiene la lógica de ciclo de vida compartida.

Un HOC no es una API especial de React, sino simplemente la composición de funciones aplicada a componentes.

Un HOC para registro creado con clases

Escribiendo el envoltorio

El siguiente HOC devuelve una nueva clase que registra cuándo se monta el componente envuelto y cada vez que se actualiza, y luego renderiza el componente original pasando todos los propios recibidos:

import React, { Component } from "react";

export function withRenderLogger(WrappedComponent) {
  return class extends Component {
    componentDidMount() {
      console.log(`${WrappedComponent.name} mounted`);
    }
    componentDidUpdate() {
      console.log(`${WrappedComponent.name} updated`);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

El contenedor gestiona los métodos del ciclo de vida y reenvía this.props sin cambios, por lo que el componente original no contiene código de registro.

Aplicándolo

Se exporta la versión envuelta en lugar de la clase original:

import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";

class Profile extends Component {
  render() {
    return <h2>Profile Component</h2>;
  }
}
export default withRenderLogger(Profile);

Al renderizar Profile ahora se muestra el contenedor, que a su vez renderiza el verdadero Profile. La clase devuelta es anónima, por lo que establecer un displayName como withRenderLogger(Profile) facilita la lectura en React DevTools.

Los costos de los HOC basados en clases

Funcionó, pero con dificultades:

  • definiciones de clases anidadas y vinculación de this
  • lógica dispersa en los métodos del ciclo de vida
  • árboles de componentes más complejos, lo que hacía que la depuración requiriera examinar los contenedores
  • colisiones de nombres de propiedades cuando varios HOC inyectan propiedades con el mismo nombre
  • Acoplamiento implícito entre el HOC y las propiedades del componente envuelto
  • La misma idea con componentes funcionales

    Sin clases, this ni métodos de ciclo de vida, un HOC se reduce a una función que devuelve un componente funcional:

    function HOC(WrappedComponent) {
      return function Enhanced(props) {
        return <WrappedComponent {...props} />;
      };
    }
    

    He aquí el registrador reescrito en ese estilo:

    export function withLogger(WrappedComponent) {
      return function Enhanced(props) {
        console.log(`${WrappedComponent.name} rendered`);
       return <WrappedComponent {...props} />;
      };
    }
    

    El uso es idéntico al de la versión basada en clases:

    import React from "react";
    import { withLogger } from "./withLogger";
    
    function Dashboard() {
      return <h2>Dashboard Component</h2>;
    }
    export default withLogger(Dashboard);
    

    Registra información en cada renderizado, pero como efecto secundario dentro del cuerpo del renderizado. React puede renderizar con más frecuencia de la que realmente actualiza el estado, y el Modo Estricto hace dos renderizaciones en el entorno de desarrollo, por lo que es normal encontrar registros duplicados; utilice useEffect para cualquier cosa que vaya más allá de una depuración rápida.

    Una pequeña aplicación de demostración

    Un botón alterna entre el Profile basado en clases y el Dashboard basado en funciones, de modo que ambos HOCs registran actividad. Al ejecutar la demostración mediante la renderización en lugar de errores, se mantiene su previsibilidad bajo Fast Refresh:

    import React, { useState } from "react";
    import Profile from "./Profile";
    import Dashboard from "./Dashboard";
    
    export default function App() {
      const [toggle, setToggle] = useState(true);
      return (
        <div style={{ padding: 20 }}>
          <h1>HOC Demo</h1>
          <button onClick={() => setToggle(!toggle)}>
            Re-render Components
          </button>
          {toggle ? <Profile /> : <Dashboard />}
        </div>
      );
    }
    

    Profile.js utiliza el HOC basado en clases:

    import React, { Component } from "react";
    import { withRenderLogger } from "./withRenderLogger";
    
    class Profile extends Component {
      render() {
        return <h2>Profile Component</h2>;
      }
    }
    export default withRenderLogger(Profile);
    

    Dashboard.js utiliza el HOC basado en funciones:

    import React from "react";
    import { withLogger } from "./withLogger";
    
    function Dashboard() {
      return <h2>Dashboard Component</h2>;
    }
    export default withLogger(Dashboard);
    

    El módulo del HOC basado en clases:

    import React, { Component } from "react";
    
    export function withRenderLogger(WrappedComponent) {
      return class extends Component {
        componentDidMount() {
          console.log(`${WrappedComponent.name} mounted`);
        }
        componentDidUpdate() {
          console.log(`${WrappedComponent.name} updated`);
        }
        render() {
          return <WrappedComponent {...this.props} />;
        }
      };
    }
    

    Y el módulo del HOC basado en funciones:

    export function withLogger(WrappedComponent) {
      return function Enhanced(props) {
        console.log(`${WrappedComponent.name} rendered`);
        return <WrappedComponent {...props} />;
      };
    }
    

    Estos archivos funcionan en cualquier configuración estándar de React; con Vite, los archivos JSX deben tener el nombre .jsx.

    Por qué los hooks personalizados reemplazaron a la mayoría de los HOCs

    Los hooks personalizados ahora cubren la mayoría de los casos de uso anteriores de los HOCs: carga de datos remotos, verificaciones de autenticación, seguimiento, listeners del DOM y suscripciones. El registrador de actividad se convierte en un hook:

    function useLogger(name) {
      useEffect(() => {
        console.log(`${name} rendered`);
      });
    }
    
    function Dashboard() {
      useLogger("Dashboard");
      return <h2>Dashboard Component</h2>;
    }
    

    Los hooks no añaden capas de envoltorio, devuelven valores en lugar de inyectar propiedades y se componen sin colisiones. Al no haber un array de dependencias, el efecto se ejecuta después de cada commit.

    Los HOC siguen siendo útiles para el envoltorio condicional (mostrar una pantalla de inicio de sesión en lugar del componente), para bibliotecas con APIs HOC y para código basado en clases; React.memo tiene la misma estructura. Aplique los HOC a nivel de módulo: crear un envoltorio durante el renderizado vuelve a montar el subárbol y hace que pierda su estado.

    Puntos clave

    • Un HOC envuelve un componente y devuelve una versión mejorada de él.
    • Los componentes de clase dependían de los HOC porque compartir la lógica del ciclo de vida de otra manera resultaría repetitivo.
    • Con los componentes funcionales, un HOC es simplemente una función que devuelve un componente funcional.
    • Los hooks personalizados son la opción por defecto hoy en día para reutilizar lógica; mantenga los HOC para el envoltorio y la renderización condicional.
  • Establezca un displayName, mantenga los efectos secundarios fuera del proceso de renderizado y nunca cree HOCs durante el renderizado.
  • Lecturas relacionadas