Higher-Order Components from Class Wrappers to Functions and Hooks
Learn how React higher-order components work, how the same logger HOC looks as a class and as a function, and why custom hooks replaced most HOC use cases.
This article is published in English.
Sources: React 27: Higher Order Components (HOC) — Deep Dive (Class vs Functional Components)
Long before custom hooks, React developers needed a way to share behavior such as logging, authentication checks or data loading across many components. The answer was the higher-order component (HOC): a plain function that receives a component and produces an enhanced wrapper around it. Below, the same logging HOC is built as a class and as a function, then compared with a custom hook.
Why HOCs appeared in the class era
When most React code was written as classes, reusing logic was awkward. Imagine Profile, Dashboard and Settings all needing to log when they mount and update, verify that the user is signed in, load some data and handle errors. Without a shared abstraction, each class repeated the same constructor, state setup, lifecycle methods and helper methods. HOCs solved this by wrapping a class in another class that owns the shared lifecycle logic.
An HOC is not a special React API, just function composition applied to components.
A logging HOC built with classes
Writing the wrapper
The following HOC returns a new class that logs when the wrapped component mounts and whenever it updates, then renders the original component with all incoming props passed through:
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} />;
}
};
}
The wrapper owns the lifecycle methods and forwards this.props unchanged, so the original component contains no logging code.
Applying it
You export the wrapped version instead of the raw class:
import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";
class Profile extends Component {
render() {
return <h2>Profile Component</h2>;
}
}
export default withRenderLogger(Profile);
Rendering Profile now renders the wrapper, which renders the real Profile. The returned class is anonymous, so setting a displayName such as withRenderLogger(Profile) makes React DevTools easier to read.
The costs of class-based HOCs
It worked, but with friction:
- nested class definitions and
thisbinding - logic spread across lifecycle methods
- deeper component trees, so debugging meant digging through wrappers
- prop name collisions when several HOCs inject props with the same name
- implicit coupling between the HOC and the wrapped component's props
The same idea with function components
Without classes, this or lifecycle methods, an HOC shrinks to a function returning a function component:
function HOC(WrappedComponent) {
return function Enhanced(props) {
return <WrappedComponent {...props} />;
};
}
Here is the logger rewritten in that style:
export function withLogger(WrappedComponent) {
return function Enhanced(props) {
console.log(`${WrappedComponent.name} rendered`);
return <WrappedComponent {...props} />;
};
}
Usage is identical to the class version:
import React from "react";
import { withLogger } from "./withLogger";
function Dashboard() {
return <h2>Dashboard Component</h2>;
}
export default withLogger(Dashboard);
It logs on every render, but as a side effect in the render body. React may render more often than it commits, and Strict Mode renders twice in development, so expect duplicate logs; use useEffect for anything beyond quick debugging.
A small demo app
A button toggles between the class-based Profile and the function-based Dashboard, so both HOCs log. Driving the demo by rendering rather than thrown errors keeps it predictable under 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 uses the class HOC:
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 uses the function HOC:
import React from "react";
import { withLogger } from "./withLogger";
function Dashboard() {
return <h2>Dashboard Component</h2>;
}
export default withLogger(Dashboard);
The class HOC module:
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} />;
}
};
}
And the function HOC module:
export function withLogger(WrappedComponent) {
return function Enhanced(props) {
console.log(`${WrappedComponent.name} rendered`);
return <WrappedComponent {...props} />;
};
}
These files run in any standard React setup; with Vite, name JSX files .jsx.
Why custom hooks replaced most HOCs
Custom hooks now cover most former HOC use cases: loading remote data, auth checks, tracking, DOM listeners and subscriptions. The logger becomes a hook:
function useLogger(name) {
useEffect(() => {
console.log(`${name} rendered`);
});
}
function Dashboard() {
useLogger("Dashboard");
return <h2>Dashboard Component</h2>;
}
Hooks add no wrapper layers, return values instead of injecting props, and compose without collisions. With no dependency array, the effect runs after every commit.
HOCs still fit conditional wrapping (showing a sign-in screen instead of the component), libraries with HOC APIs and class-based code; React.memo has the same shape. Apply HOCs at module level: creating a wrapper during render remounts the subtree and loses its state.
Key takeaways
- An HOC wraps a component and hands back an enhanced version of it.
- Class components relied on HOCs because sharing lifecycle logic was otherwise repetitive.
- With function components, an HOC is just a function returning a function component.
- Custom hooks are the default choice for reusing logic today; keep HOCs for wrapping and conditional rendering.
- Set a
displayName, keep side effects out of render, and never create HOCs during render.