Higher-Order Components (HOC)
Higher-Order Components (HOC) are advanced React patterns that allow you to reuse component logic by wrapping components with additional functionality.
Table of Contents
What is a HOC?
A Higher-Order Component is a function that takes a component and returns a new component with enhanced functionality.
Key Concepts:
- Function that returns a component: HOC is a function, not a component
- Composition over inheritance: Reuse logic through composition
- Props manipulation: Can add, modify, or filter props
- Cross-cutting concerns: Handle common functionality across components
Basic HOC Structure:
// HOC is a function that takes a component and returns a new component
function withEnhancement(WrappedComponent) {
// Return a new component
return function EnhancedComponent(props) {
// Add logic here
const enhancedProps = {
...props,
enhanced: true
};
// Return the wrapped component with enhanced props
return <WrappedComponent {...enhancedProps} />;
};
}
// Usage
const EnhancedButton = withEnhancement(Button);
HOC Pattern
1. Basic HOC:
// Simple HOC that adds a prop
function withGreeting(WrappedComponent) {
return function EnhancedComponent(props) {
return (
<WrappedComponent
{...props}
greeting="Hello from HOC!"
/>
);
};
}
// Original component
function Button({ onClick, children, greeting }) {
return (
<button onClick={onClick}>
{greeting} - {children}
</button>
);
}
// Enhanced component
const EnhancedButton = withGreeting(Button);
// Usage
function App() {
return (
<EnhancedButton onClick={() => alert('Clicked!')}>
Click me
</EnhancedButton>
);
}
2. HOC with State:
// HOC that manages state
function withCounter(WrappedComponent) {
return class EnhancedComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
};
render() {
return (
<WrappedComponent
{...this.props}
count={this.state.count}
increment={this.increment}
/>
);
}
};
}
// Component that uses the counter
function DisplayCounter({ count, increment, label }) {
return (
<div>
<h3>{label}</h3>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// Enhanced component
const CounterDisplay = withCounter(DisplayCounter);
// Usage
function App() {
return (
<div>
<CounterDisplay label="First Counter" />
<CounterDisplay label="Second Counter" />
</div>
);
}
3. HOC with Props Transformation:
// HOC that transforms props
function withUpperCase(WrappedComponent) {
return function EnhancedComponent(props) {
// Transform text props to uppercase
const transformedProps = Object.keys(props).reduce((acc, key) => {
if (typeof props[key] === 'string') {
acc[key] = props[key].toUpperCase();
} else {
acc[key] = props[key];
}
return acc;
}, {});
return <WrappedComponent {...transformedProps} />;
};
}
// Original component
function TextDisplay({ title, description }) {
return (
<div>
<h2>{title}</h2>
<p>{description}</p>
</div>
);
}
// Enhanced component
const UpperCaseText = withUpperCase(TextDisplay);
// Usage
function App() {
return (
<UpperCaseText
title="hello world"
description="this will be uppercase"
/>
);
// Renders: "HELLO WORLD" and "THIS WILL BE UPPERCASE"
}
Common HOC Examples
1. withLoading HOC:
// HOC that handles loading state
function withLoading(WrappedComponent) {
return function EnhancedComponent({ loading, ...props }) {
if (loading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// Component that needs loading state
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Enhanced component
const UserProfileWithLoading = withLoading(UserProfile);
// Usage
function App() {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(user => {
setUser(user);
setLoading(false);
});
}, []);
return (
<UserProfileWithLoading
loading={loading}
user={user}
/>
);
}
2. withErrorBoundary HOC:
// HOC that provides error boundary functionality
function withErrorBoundary(WrappedComponent) {
return class EnhancedComponent extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by HOC:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return <WrappedComponent {...this.props} />;
}
};
}
// Component that might throw errors
function RiskyComponent({ data }) {
if (!data) {
throw new Error('Data is required');
}
return <div>{data}</div>;
}
// Enhanced component
const SafeComponent = withErrorBoundary(RiskyComponent);
// Usage
function App() {
return (
<SafeComponent data={null} /> // Will show error boundary
);
}
3. withAuthentication HOC:
// HOC that handles authentication
function withAuthentication(WrappedComponent) {
return function EnhancedComponent(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
// Check authentication status
checkAuth().then(authData => {
setIsAuthenticated(authData.isAuthenticated);
setUser(authData.user);
});
}, []);
if (!isAuthenticated) {
return <LoginForm />;
}
return (
<WrappedComponent
{...props}
user={user}
isAuthenticated={isAuthenticated}
/>
);
};
}
// Component that needs authentication
function Dashboard({ user, isAuthenticated }) {
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>You are authenticated: {isAuthenticated ? 'Yes' : 'No'}</p>
</div>
);
}
// Enhanced component
const AuthenticatedDashboard = withAuthentication(Dashboard);
// Usage
function App() {
return <AuthenticatedDashboard />;
}
Advanced HOC Patterns
1. HOC with Display Name:
// HOC that sets proper display name for debugging
function withDisplayName(WrappedComponent, displayName) {
const EnhancedComponent = function(props) {
return <WrappedComponent {...props} />;
};
// Set display name for debugging
EnhancedComponent.displayName = `${displayName}(${getDisplayName(WrappedComponent)})`;
return EnhancedComponent;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
// Usage
const EnhancedButton = withDisplayName(Button, 'withGreeting');
// Display name will be: "withGreeting(Button)"
2. HOC with Ref Forwarding:
// HOC that forwards refs
function withRef(WrappedComponent) {
return React.forwardRef((props, ref) => {
return <WrappedComponent {...props} ref={ref} />;
});
}
// Component that accepts ref
const Button = React.forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
// Enhanced component
const EnhancedButton = withRef(Button);
// Usage
function App() {
const buttonRef = useRef();
useEffect(() => {
buttonRef.current.focus();
}, []);
return <EnhancedButton ref={buttonRef}>Click me</EnhancedButton>;
}
3. HOC with Multiple Enhancements:
// Compose multiple HOCs
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
// Multiple HOCs
const withLogging = (WrappedComponent) => {
return function EnhancedComponent(props) {
console.log('Component rendered with props:', props);
return <WrappedComponent {...props} />;
};
};
const withTiming = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
componentDidMount() {
console.time('Component render time');
}
componentDidUpdate() {
console.timeEnd('Component render time');
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};
// Compose HOCs
const EnhancedComponent = compose(
withLogging,
withTiming,
withCounter
)(Button);
Best Practices
1. Don’t Mutate the Wrapped Component:
// Good: Don't mutate the original component
function withEnhancement(WrappedComponent) {
return function EnhancedComponent(props) {
return <WrappedComponent {...props} />;
};
}
// Bad: Don't mutate the original component
function withEnhancement(WrappedComponent) {
WrappedComponent.newProp = 'value'; // Don't do this
return WrappedComponent;
}
2. Pass Through Unrelated Props:
// Good: Pass through all props
function withEnhancement(WrappedComponent) {
return function EnhancedComponent(props) {
const { enhanced, ...restProps } = props;
return <WrappedComponent {...restProps} enhanced={enhanced} />;
};
}
// Bad: Don't filter out props unnecessarily
function withEnhancement(WrappedComponent) {
return function EnhancedComponent({ enhanced, ...props }) {
return <WrappedComponent enhanced={enhanced} />; // Lost other props
};
}
3. Use Display Names for Debugging:
// Good: Set display name
function withEnhancement(WrappedComponent) {
const EnhancedComponent = function(props) {
return <WrappedComponent {...props} />;
};
EnhancedComponent.displayName = `withEnhancement(${getDisplayName(WrappedComponent)})`;
return EnhancedComponent;
}
4. Consider Using Hooks Instead:
// Modern approach: Custom hooks
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
// Usage
function Counter() {
const { count, increment } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// vs HOC approach
const CounterWithHOC = withCounter(Counter);
5. Handle Refs Properly:
// Good: Forward refs when needed
function withRef(WrappedComponent) {
return React.forwardRef((props, ref) => {
return <WrappedComponent {...props} ref={ref} />;
});
}
Common Interview Questions
Q: What is a Higher-Order Component (HOC)?
- A function that takes a component and returns a new component with enhanced functionality.
Q: What are the main benefits of using HOCs?
- Code reuse, separation of concerns, and the ability to add functionality to components without modifying them.
Q: How do you create a basic HOC?
- Create a function that takes a component as an argument and returns a new component with additional props or logic.
Q: What are some common use cases for HOCs?
- Authentication, loading states, error boundaries, logging, and cross-cutting concerns.
Q: What are the limitations of HOCs?
- Props drilling, wrapper hell, and potential naming conflicts.
Q: How do you handle refs in HOCs?
- Use React.forwardRef to properly forward refs to the wrapped component.
Q: What’s the difference between HOCs and custom hooks?
- HOCs are class-based patterns that wrap components, while custom hooks are function-based and can be used directly in components.
Q: How do you compose multiple HOCs?
- Use a compose function or nest them:
withA(withB(withC(Component))).
Q: What are some alternatives to HOCs?
- Custom hooks, render props, and component composition.
Q: How do you debug HOCs?
- Set proper display names and use React DevTools to inspect the component hierarchy.
Summary
- HOCs are functions that take components and return enhanced components
- Common patterns include withLoading, withErrorBoundary, and withAuthentication
- Best practices include not mutating wrapped components and passing through props
- Modern alternative is custom hooks for most use cases
- Ref forwarding is important when the wrapped component needs refs
- Composition allows combining multiple HOCs
- Display names help with debugging
- Understanding HOCs is crucial for advanced React patterns and legacy code
Interview angle
- “Is the HOC pattern still relevant?” - mostly historical. Hooks cover the same need - sharing stateful logic - without wrapper nesting, prop-name collisions or the lost-static-methods problem. You still meet HOCs in older code and in libraries (
connect, error-boundary wrappers,React.memoitself). - “What were the problems?” - wrapper hell in the tree, implicit props whose origin is invisible at the call site, ref forwarding needing extra work, and no type-safe way to express “adds these props” in older TypeScript.
- “HOC, render prop, or hook?” - hook by default. Render props still have a place when the shared thing is rendering rather than logic. HOC when you must wrap a class component or conform to an existing library API.