A Higher-Order Component (HOC) — is a function that takes a component and returns a new component with enhanced functionality. It’s a composition pattern for reusing component logic.
Basic HOC example:
// HOC for adding logging
const withLogging = (WrappedComponent) => {
return function WithLoggingComponent(props) {
useEffect(() => {
console.log('Component mounted:', WrappedComponent.name);
}, []);
return <WrappedComponent {...props} />;
};
};
// Usage
const Button = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);
const ButtonWithLogging = withLogging(Button);
HOCs — are an advanced pattern in React for reusing component logic. They are not part of the React API, but emerge from React’s compositional nature.
// HOC takes a component
const withEnhancement = (WrappedComponent) => {
// Returns a new component
const EnhancedComponent = (props) => {
// Adds new logic
const [state, setState] = useState();
// Renders the original component with new props
return (
<WrappedComponent
{...props}
enhancedProp={state}
/>
);
};
// Set displayName for debugging
EnhancedComponent.displayName =
`withEnhancement(${WrappedComponent.displayName || WrappedComponent.name})`;
return EnhancedComponent;
};
const withAuth = (WrappedComponent) => {
return function WithAuthComponent(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check authentication
checkAuth()
.then(authenticated => {
setIsAuthenticated(authenticated);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} />;
};
};
// Usage
const Dashboard = () => <div>Dashboard</div>;
const ProtectedDashboard = withAuth(Dashboard);
const withDataFetching = (url) => (WrappedComponent) => {
return function WithDataFetchingComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
return (
<WrappedComponent
{...props}
data={data}
loading={loading}
error={error}
/>
);
};
};
// Usage
const UserList = ({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const UsersWithData = withDataFetching('/api/users')(UserList);
const withWindowSize = (WrappedComponent) => {
return function WithWindowSizeComponent(props) {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<WrappedComponent
{...props}
windowSize={windowSize}
/>
);
};
};
// Usage
const ResponsiveComponent = ({ windowSize }) => (
<div>
Width: {windowSize.width}px
{windowSize.width < 768 ? ' (Mobile)' : ' (Desktop)'}
</div>
);
const ResponsiveWithSize = withWindowSize(ResponsiveComponent);
// Multiple HOCs in sequence
const enhance = compose(
withAuth,
withWindowSize,
withLogging
);
const EnhancedComponent = enhance(BaseComponent);
// Or manually
const EnhancedComponent =
withAuth(
withWindowSize(
withLogging(BaseComponent)
)
);
const compose = (...hocs) => (Component) =>
hocs.reduceRight((acc, hoc) => hoc(acc), Component);
// Usage
const enhance = compose(
withRouter,
withAuth,
connect(mapStateToProps)
);
const EnhancedApp = enhance(App);
const withTheme = (theme = 'light') => (WrappedComponent) => {
return function WithThemeComponent(props) {
const themeConfig = {
light: {
background: '#fff',
color: '#000'
},
dark: {
background: '#000',
color: '#fff'
}
};
return (
<div style={themeConfig[theme]}>
<WrappedComponent {...props} theme={theme} />
</div>
);
};
};
// Usage
const ThemedButton = withTheme('dark')(Button);
const withRefForwarding = (WrappedComponent) => {
const WithRef = React.forwardRef((props, ref) => {
return <WrappedComponent {...props} forwardedRef={ref} />;
});
WithRef.displayName =
`withRefForwarding(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithRef;
};
// Component using ref
const FancyInput = ({ forwardedRef, ...props }) => (
<input ref={forwardedRef} {...props} />
);
const ForwardedInput = withRefForwarding(FancyInput);
// Usage
const App = () => {
const inputRef = useRef();
return <ForwardedInput ref={inputRef} />;
};
// ❌ Bad: mutating the component
const withBadEnhancement = (WrappedComponent) => {
WrappedComponent.prototype.componentDidUpdate = function() {
console.log('Updated!');
};
return WrappedComponent;
};
// ✅ Good: composition
const withGoodEnhancement = (WrappedComponent) => {
return class extends React.Component {
componentDidUpdate() {
console.log('Updated!');
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};
// ❌ Bad: losing props
const withBadHOC = (WrappedComponent) => {
return function BadHOC({ someProp }) {
return <WrappedComponent someProp={someProp} />;
};
};
// ✅ Good: passing all props
const withGoodHOC = (WrappedComponent) => {
return function GoodHOC(props) {
return <WrappedComponent {...props} />;
};
};
// ❌ Bad: HOC does too much
const withEverything = (WrappedComponent) => {
return function WithEverything(props) {
// Authentication
// Data loading
// Theme
// Logging
// ...too much logic
};
};
// ✅ Good: separate HOCs for each concern
const enhance = compose(
withAuth,
withDataFetching('/api/data'),
withTheme('dark'),
withLogging
);
// HOC
const withMouse = (Component) => (props) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ... logic
return <Component {...props} mouse={position} />;
};
// Render Props
const Mouse = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ... logic
return render(position);
};
// Using Render Props
<Mouse render={position => <Component mouse={position} />} />
// HOC
const withWindowSize = (Component) => (props) => {
const [size, setSize] = useState(getWindowSize());
// ... logic
return <Component {...props} windowSize={size} />;
};
// Custom Hook
const useWindowSize = () => {
const [size, setSize] = useState(getWindowSize());
// ... logic
return size;
};
// Using Hook
const Component = () => {
const windowSize = useWindowSize();
// ... use windowSize
};
✅ Good for:
❌ Not good when:
// Setting displayName for DevTools
const withDebugInfo = (WrappedComponent) => {
const WithDebugInfo = (props) => {
useEffect(() => {
console.log(`${WrappedComponent.name} rendered with props:`, props);
});
return <WrappedComponent {...props} />;
};
// Important for React DevTools
WithDebugInfo.displayName =
`withDebugInfo(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithDebugInfo;
};
HOCs remain a powerful tool for creating reusable logic, although in modern React, hooks are often preferred for solving the same problems.
Want more interview prep articles? Subscribe to EasyAdvice, bookmark the site, and improve every day 💪