PatternsArchitecture Patterns
Higher-Order Components
Enhance components with reusable behavior using HOC pattern
Higher-Order Components (HOC)
A Higher-Order Component is a function that takes a component and returns a new component with enhanced functionality. While hooks are now preferred, HOCs are still used in many production codebases.
🎯 What is an HOC?
// HOC is a function that takes a component
function withAuth(Component) {
// And returns a new component
return function AuthenticatedComponent(props) {
const { user } = useAuth();
if (!user) {
return <Redirect to="/login" />;
}
return <Component {...props} user={user} />;
};
}
// Usage
const ProtectedPage = withAuth(Dashboard);📊 HOC vs Hooks vs Render Props
| Feature | HOC | Hooks | Render Props |
|---|---|---|---|
| Reusability | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Composability | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Type Safety | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Debugging | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Wrapper Hell | ❌ Yes | ✅ No | ❌ Yes |
| Props Collision | ⚠️ Possible | ✅ No | ✅ No |
🔧 Basic HOC Pattern
Simple HOC
interface WithLoadingProps {
loading: boolean;
}
function withLoading<P extends object>(
Component: React.ComponentType<P>
) {
return function WithLoadingComponent(
props: P & WithLoadingProps
) {
const { loading, ...rest } = props;
if (loading) {
return <div>Loading...</div>;
}
return <Component {...(rest as P)} />;
};
}
// Usage
interface UserListProps {
users: User[];
}
function UserList({ users }: UserListProps) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
const UserListWithLoading = withLoading(UserList);
// Now you can use it with loading prop
<UserListWithLoading
users={users}
loading={isLoading}
/>HOC with Options
interface WithDataOptions<TData = unknown> {
url: string;
transform?: (data: unknown) => TData;
}
function withData<P extends { data: unknown }>(
options: WithDataOptions
) {
return function (Component: React.ComponentType<P>) {
return function WithDataComponent(props: Omit<P, 'data'>) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(options.url)
.then(res => res.json())
.then(data => {
const transformed = options.transform
? options.transform(data)
: data;
setData(transformed);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return <Component {...(props as P)} data={data} />;
};
};
}
// Usage
interface ProductListProps {
data: Product[];
}
const ProductList = withData<ProductListProps>({
url: '/api/products',
transform: (data) => data.filter(p => p.inStock)
})(function ProductList({ data }) {
return (
<ul>
{data.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
});🎨 Common HOC Patterns
1. Authentication HOC
interface WithAuthProps {
user: User;
logout: () => void;
}
function withAuth<P extends WithAuthProps>(
Component: React.ComponentType<P>
) {
return function AuthenticatedComponent(
props: Omit<P, keyof WithAuthProps>
) {
const { user, logout } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
return (
<Component
{...(props as P)}
user={user}
logout={logout}
/>
);
};
}
// Usage
interface DashboardProps extends WithAuthProps {
title: string;
}
const Dashboard = withAuth<DashboardProps>(
function Dashboard({ user, logout, title }) {
return (
<div>
<h1>{title}</h1>
<p>Welcome, {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
);2. Permission-Based HOC
function withPermission<P extends object>(
requiredPermission: string
) {
return function (Component: React.ComponentType<P>) {
return function PermissionComponent(props: P) {
const { user } = useAuth();
if (!user?.permissions.includes(requiredPermission)) {
return <div>Access Denied</div>;
}
return <Component {...props} />;
};
};
}
// Usage
const AdminPanel = withPermission('admin')(
function AdminPanel() {
return <div>Admin Content</div>;
}
);
const ModeratorPanel = withPermission('moderator')(
function ModeratorPanel() {
return <div>Moderator Content</div>;
}
);3. Analytics HOC
interface WithAnalyticsOptions {
eventName: string;
category?: string;
}
function withAnalytics<P extends object>(
options: WithAnalyticsOptions
) {
return function (Component: React.ComponentType<P>) {
return function AnalyticsComponent(props: P) {
const { eventName, category = 'general' } = options;
useEffect(() => {
// Track page view
analytics.track(eventName, {
category,
timestamp: Date.now(),
path: window.location.pathname
});
}, []);
return <Component {...props} />;
};
};
}
// Usage
const ProductPage = withAnalytics({
eventName: 'product_page_view',
category: 'ecommerce'
})(function ProductPage({ productId }) {
return <div>Product {productId}</div>;
});4. Error Boundary HOC
function withErrorBoundary<P extends object>(
FallbackComponent?: React.ComponentType<{ error: Error }>
) {
return function (Component: React.ComponentType<P>) {
return class ErrorBoundary extends React.Component<
P,
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Error caught:', error, info);
}
render() {
if (this.state.hasError) {
if (FallbackComponent) {
return <FallbackComponent error={this.state.error!} />;
}
return <div>Something went wrong</div>;
}
return <Component {...this.props} />;
}
};
};
}
// Usage
const SafeComponent = withErrorBoundary(
function ErrorFallback({ error }) {
return <div>Error: {error.message}</div>;
}
)(function UnsafeComponent() {
// Component that might throw
return <div>Content</div>;
});🔄 Composing Multiple HOCs
Manual Composition
const EnhancedComponent = withAuth(
withAnalytics({
eventName: 'dashboard_view'
})(
withErrorBoundary()(
Dashboard
)
)
);
// Hard to read! 😱Using compose Utility
import { compose } from 'redux'; // or create your own
const enhance = compose(
withAuth,
withAnalytics({ eventName: 'dashboard_view' }),
withErrorBoundary()
);
const EnhancedDashboard = enhance(Dashboard);
// Much better! ✨Custom compose Function
type Fn = (x: unknown) => unknown;
function compose<T>(...fns: Fn[]) {
return (x: T): unknown => fns.reduceRight((acc, fn) => fn(acc), x as unknown);
}
// Or left-to-right (pipe)
function pipe<T>(...fns: Fn[]) {
return (x: T): unknown => fns.reduce((acc, fn) => fn(acc), x as unknown);
}
// Usage
const enhance = pipe(
withAuth,
withData({ url: '/api/user' }),
withAnalytics({ eventName: 'view' })
);⚠️ Common Pitfalls
1. Don't Mutate Original Component
// ❌ BAD: Mutating original
function withExtra(Component) {
Component.prototype.extra = function() {
// DON'T DO THIS
};
return Component;
}
// ✅ GOOD: Return new component
function withExtra(Component) {
return function Enhanced(props) {
const extra = () => {
// Extra functionality
};
return <Component {...props} extra={extra} />;
};
}2. Copy Static Methods
import hoistNonReactStatics from 'hoist-non-react-statics';
function withAuth(Component) {
function AuthComponent(props) {
// ... auth logic
return <Component {...props} />;
}
// Copy static methods
hoistNonReactStatics(AuthComponent, Component);
return AuthComponent;
}3. Pass Through Refs
function withLogging<P extends object>(
Component: React.ComponentType<P>
) {
function LoggingComponent(props: P & { forwardedRef?: React.Ref<unknown> }) {
const { forwardedRef, ...rest } = props;
useEffect(() => {
console.log('Component rendered');
});
return <Component ref={forwardedRef} {...(rest as P)} />;
}
return React.forwardRef((props: P, ref) => {
return <LoggingComponent {...props} forwardedRef={ref} />;
});
}4. Display Name for Debugging
function withAuth<P extends object>(Component: React.ComponentType<P>) {
function AuthComponent(props: P) {
// ... implementation
}
// Set display name for React DevTools
const displayName = Component.displayName || Component.name || 'Component';
AuthComponent.displayName = `withAuth(${displayName})`;
return AuthComponent;
}🆚 HOC vs Hooks Migration
Before (HOC)
const UserProfile = withAuth(
withData({ url: '/api/user' })(
withTheme(
function UserProfile({ user, data, theme }) {
return (
<div style={{ background: theme.background }}>
<h1>{user.name}</h1>
<p>{data.bio}</p>
</div>
);
}
)
)
);After (Hooks)
function UserProfile() {
const { user } = useAuth();
const { data } = useFetch('/api/user');
const theme = useTheme();
if (!user) return <Navigate to="/login" />;
return (
<div style={{ background: theme.background }}>
<h1>{user.name}</h1>
<p>{data.bio}</p>
</div>
);
}
// Cleaner, easier to understand! ✨🏢 Real-World Examples
Redux connect (Legacy HOC)
import { connect } from 'react-redux';
const mapStateToProps = (state) => ({
user: state.user,
posts: state.posts
});
const mapDispatchToProps = (dispatch) => ({
login: () => dispatch(loginAction())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Dashboard);React Router withRouter (Legacy)
import { withRouter } from 'react-router-dom';
function MyComponent({ history, location, match }) {
return <div>Current path: {location.pathname}</div>;
}
export default withRouter(MyComponent);
// Now use hooks instead:
import { useHistory, useLocation, useParams } from 'react-router-dom';📚 Key Takeaways
- Hooks are preferred - Use for new code
- HOCs still valid - Legacy codebases use them
- Composition is tricky - Can create wrapper hell
- Type safety is harder - With TypeScript
- Props collision - Be careful with prop names
- Use for cross-cutting concerns - Auth, logging, etc
- Consider alternatives - Hooks, render props, compound components
Modern approach: Convert HOCs to hooks when refactoring. Only use HOCs for cross-cutting concerns where hooks don't fit.