Componentes de Orden Superior — Mejora componentes con comportamiento compartido envolviéndolos en una función
Receta
// HOC que añade protección de autenticación
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
function AuthenticatedComponent(props: P) {
const { user, isLoading } = useAuth();
if (isLoading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;
return <WrappedComponent {...props} />;
}
AuthenticatedComponent.displayName =
`withAuth(${WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"})`;
return AuthenticatedComponent;
}
// Uso
const ProtectedDashboard = withAuth(Dashboard);Cuándo usarlo: Cuando necesitas aplicar la misma preocupación transversal (autenticación, logging, temas, obtención de datos) a muchos componentes sin modificarlos. Menos común hoy gracias a los hooks, pero aún útil para envoltorios a nivel de ruta e integración de librerías de terceros.
Ejemplo Funcional
import { useEffect, useRef, type ComponentType } from "react";
// HOC que rastrea la visibilidad de los componentes y reporta analítica
function withVisibilityTracking<P extends { id: string }>(
WrappedComponent: ComponentType<P>,
eventName: string
) {
function TrackedComponent(props: P) {
const ref = useRef<HTMLDivElement>(null);
const reported = useRef(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !reported.current) {
reported.current = true;
analytics.track(eventName, { componentId: props.id });
}
},
{ threshold: 0.5 }
);
observer.observe(element);
return () => observer.disconnect();
}, [props.id]);
return (
<div ref={ref}>
<WrappedComponent {...props} />
</div>
);
}
TrackedComponent.displayName =
`withVisibilityTracking(${WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"})`;
return TrackedComponent;
}
// Uso
interface ProductCardProps {
id: string;
name: string;
price: number;
}
function ProductCard({ id, name, price }: ProductCardProps) {
return (
<div className="p-4 border rounded">
<h3>{name}</h3>
<p>${price}</p>
</div>
);
}
const TrackedProductCard = withVisibilityTracking(ProductCard, "product_viewed");
// En una página
function ProductGrid({ products }: { products: ProductCardProps[] }) {
return (
<div className="grid grid-cols-3 gap-4">
{products.map((p) => (
<TrackedProductCard key={p.id} {...p} />
))}
</div>
);
}Lo que esto demuestra:
- HOC que añade rastreo con IntersectionObserver sin modificar el componente original
- Restricción de tipo genérico (
P extends { id: string }) asegura que el componente envuelto tenga los props requeridos displayNameestablecido para depuración en React DevTools- El
ProductCardoriginal permanece puro y comprobable por sí solo
Análisis Profundo
Cómo Funciona
- Un HOC es una función que toma un componente y devuelve un nuevo componente con comportamiento mejorado.
- El patrón sigue el concepto matemático de composición de funciones:
enhance(Component) => EnhancedComponent. - El HOC no muta el componente original — lo envuelve en uno nuevo.
- El componente envolvente puede interceptar props, inyectar nuevos props, renderizar condicionalmente, o añadir efectos de ciclo de vida.
- Múltiples HOCs pueden ser compuestos:
withAuth(withTheme(withAnalytics(Component))).
Parámetros y Valores de Retorno
| Parámetro | Tipo | Propósito |
|---|---|---|
WrappedComponent | ComponentType<P> | El componente a mejorar |
| Configuración (opcional) | Varía | Configuración para el comportamiento del HOC |
| Valor de retorno | ComponentType<P> (o modificado) | Un nuevo componente con comportamiento añadido |
Variaciones
HOC de inyección de props — añade nuevos props al componente envuelto:
interface WithThemeProps {
theme: Theme;
}
function withTheme<P extends WithThemeProps>(
WrappedComponent: ComponentType<P>
) {
function ThemedComponent(props: Omit<P, keyof WithThemeProps>) {
const theme = useTheme();
return <WrappedComponent {...(props as P)} theme={theme} />;
}
ThemedComponent.displayName =
`withTheme(${WrappedComponent.displayName ?? WrappedComponent.name})`;
return ThemedComponent;
}HOCs compuestos — combina múltiples mejoras:
// Composición manual
const EnhancedComponent = withAuth(withTheme(withAnalytics(BaseComponent)));
// Con una utilidad de composición
import { compose } from "redux"; // o escribe la tuya
const enhance = compose(withAuth, withTheme, withAnalytics);
const EnhancedComponent = enhance(BaseComponent);Notas de TypeScript
- Usa
ComponentType<P>para aceptar componentes de función y clase. - Usa
Omit<P, keyof InjectedProps>para eliminar props inyectadas de la API externa. - Siempre restringe
P extends objectal mínimo para evitar tipos primitivos. - Establece
displayNameen cada HOC para que React DevTools sea usable. - Considera usar
React.forwardRefdentro de HOCs si el componente envuelto necesita reenvío de refs.
Trampas
-
Reenvío de refs — Los refs no se pasan automáticamente a través de HOCs porque
refno es un prop regular. Solución: UsaReact.forwardRefdentro del envolvente del HOC. -
Métodos estáticos perdidos — Las propiedades estáticas en el componente original no se copian automáticamente al envolvente. Solución: Usa
hoist-non-react-staticso copia manualmente las propiedades estáticas necesarias. -
Colisiones de nombres de props — Si el HOC inyecta un prop con el mismo nombre que un prop existente, sobrescribe silenciosamente. Solución: Usa nombres de espacio para props inyectados o un prefijo único.
-
Re-crear el HOC en cada renderizado — Llamar un HOC dentro de un cuerpo de componente crea un nuevo tipo de componente en cada renderizado, destruyendo todo el estado. Solución: Siempre llama HOCs en el ámbito del módulo o en
useMemocon extrema precaución. -
Dificultad para depurar — HOCs profundamente anidados crean árboles de componentes largos en DevTools. Solución: Establece
displayNameen cada HOC y considera si los hooks serían más claros.
Alternativas
| Enfoque | Compensación |
|---|---|
| Componentes de orden superior | Mejora transparente; tipado complejo, depuración más difícil |
| Hooks personalizados | Más simples, componibles, mejores tipos; no pueden envolver renderizado |
| Render props | Flujo de datos explícito; más verboso en el sitio de llamada |
| Middleware (Next.js) | Mejor para preocupaciones a nivel de ruta como autenticación en aplicaciones Next.js |
| Decoradores (stage 3) | Azúcar sintáctico; aún no ampliamente soportado en el ecosistema React |
FAQs
¿Qué exactamente es un componente de orden superior (HOC)?
- Un HOC es una función que toma un componente y devuelve un nuevo componente mejorado.
- Sigue el patrón:
enhance(Component) => EnhancedComponent. - El HOC envuelve el componente original sin mutarlo.
¿Por qué los HOCs son menos comunes en React moderno?
- Los hooks personalizados pueden lograr la misma reutilización de comportamiento con una API más simple y componible.
- Los hooks son más fáciles de tipificar en TypeScript y producen árboles de componentes más limpios en DevTools.
- Los HOCs aún son útiles para envoltorios a nivel de ruta e integración de librerías de terceros.
¿Cómo compones múltiples HOCs juntos?
// Composición manual
const Enhanced = withAuth(withTheme(withAnalytics(Base)));
// Con una utilidad de composición
const enhance = compose(withAuth, withTheme, withAnalytics);
const Enhanced = enhance(Base);- Los HOCs pueden ser anidados o combinados con una utilidad
compose. - La composición se lee de derecha a izquierda:
withAnalyticsse aplica primero, luegowithTheme, luegowithAuth.
¿Por qué es displayName importante para HOCs?
- Sin
displayName, React DevTools muestra el componente envolvente como "Anonymous" o el nombre interno del envolvente. - Establecer
displayNameawithAuth(Dashboard)hace que el árbol de componentes sea legible. - Siempre establécelo usando la propiedad
displayNameonamedel componente envuelto.
Trampa: ¿Por qué llamar un HOC dentro de un cuerpo de componente destruye el estado?
- Llamar un HOC dentro del renderizado crea un nuevo tipo de componente en cada renderizado.
- React trata cada nuevo tipo como un componente diferente y lo desmonta/remonta, destruyendo todo el estado.
- Solución: siempre llama HOCs en el ámbito del módulo, fuera de cualquier componente.
Trampa: ¿Por qué los refs no se pasan automáticamente a través de HOCs?
refno es un prop regular en React; se maneja especialmente y se extrae antes de llegar al componente envuelto.- El envolvente del HOC recibe el ref, no el componente interno.
- Solución: usa
React.forwardRefdentro del HOC para reenviar refs al componente envuelto.
¿Cómo tipificas un HOC que inyecta props en TypeScript?
interface WithThemeProps { theme: Theme; }
function withTheme<P extends WithThemeProps>(
WrappedComponent: ComponentType<P>
) {
function Themed(props: Omit<P, keyof WithThemeProps>) {
const theme = useTheme();
return <WrappedComponent {...(props as P)} theme={theme} />;
}
return Themed;
}- Usa
Omit<P, keyof InjectedProps>para eliminar nombres de props inyectados del tipo que ve el consumidor. - Restringe
P extends InjectedPropspara que TypeScript sepa que el componente envuelto acepta esos props.
¿Cómo usas Omit para excluir props inyectados de la API externa de un HOC en TypeScript?
Omit<P, keyof InjectedProps>elimina los nombres de props inyectados del tipo que ve el consumidor.- El consumidor no necesita pasar
themecuando usawithTheme(Component)porque el HOC lo proporciona. - Siempre restringe
P extends objectal mínimo para prevenir parámetros de tipo primitivo.
¿Qué pasa con los métodos estáticos cuando envuelves un componente con un HOC?
- Las propiedades estáticas en el componente original no se copian automáticamente al envolvente del HOC.
- Solución: usa el paquete
hoist-non-react-staticso copia manualmente las propiedades estáticas necesarias. - Esta es una fuente común de errores cuando el componente envuelto tiene métodos estáticos como
getStaticProps.
¿Cuáles son las alternativas a los HOCs para aplicar preocupaciones transversales?
- Hooks personalizados: más simples, componibles, mejor soporte de TypeScript, pero no pueden envolver renderizado.
- Render props: flujo de datos explícito, más verboso en el sitio de llamada.
- Middleware de Next.js: mejor para preocupaciones a nivel de ruta como autenticación.
- Proveedores de contexto: buenos para inyección de dependencias sin envolver.
¿Cómo pueden las colisiones de nombres de props causar errores silenciosos en HOCs?
- Si un HOC inyecta un prop con el mismo nombre que un prop existente, el valor inyectado sobrescribe silenciosamente el valor del consumidor.
- Esto es difícil de depurar porque no hay error ni advertencia.
- Solución: usa nombres de espacio para props inyectados (p. ej.,
themeConfigen lugar deconfig) o un prefijo único.
Relacionado
- Render Props — Alternativa que da a los consumidores control de renderizado
- Composition — Preferido sobre HOCs cuando el comportamiento no es transversal
- Context Patterns — Frecuentemente reemplaza HOCs para inyección de dependencias