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.
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
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.
displayName, mantenga los efectos secundarios fuera del proceso de renderizado y nunca cree HOCs durante el renderizado.Lecturas relacionadas
- Escribiendo Hook de React: useState, useEffect, useReducer y Hook personalizados — Aprenda cómo escribir correctamente useState, useEffect, useReducer y hook personalizados en TypeScript, además de cuándo elegir TypeScript en lugar de JavaScript puro realmente compensa.
- Mantener a los Hook sin Cabeza: Separando el Comportamiento de la Alerta del Renderizado de la Alerta — 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.
- React Props para principiantes: Convertir componentes estáticos en plantillas — Aprenda cómo los props de React transmiten datos de los componentes padre a los hijos, cómo funciona el objeto props, qué tipos de valores se pueden pasar y por qué el flujo de datos unidireccional es útil.
- De JSX a objetos simples: Cómo funcionan los elementos, componentes y props de React — Descubra en qué se compila JSX, por qué los componentes son simplemente funciones y cómo los props, hijos, valores predeterminados, la expansión y las ranuras se derivan de ese modelo conceptual.