Higher-Order Components
Higher-Order Components (HOCs)
Higher-Order Components are a pattern for reusing component logic. A HOC is a function that takes a component and returns a new component with additional functionality.
Basic HOC Pattern
Simple HOC Example
// Basic HOC that adds loading functionality
function withLoading(WrappedComponent) {
return function WithLoadingComponent(props) {
if (props.isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// Component to enhance
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Enhanced component
const UserProfileWithLoading = withLoading(UserProfile);
// Usage
function App() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchUser().then(userData => {
setUser(userData);
setIsLoading(false);
});
}, []);
return (
<UserProfileWithLoading
user={user}
isLoading={isLoading}
/>
);
}
Authentication HOC
Protecting Routes with HOC
function withAuth(WrappedComponent, allowedRoles = []) {
return function AuthenticatedComponent(props) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
const checkAuth = async () => {
try {
const token = localStorage.getItem('authToken');
if (!token) {
navigate('/login');
return;
}
const userData = await verifyToken(token);
// Check role-based access
if (allowedRoles.length > 0 && !allowedRoles.includes(userData.role)) {
navigate('/unauthorized');
return;
}
setUser(userData);
} catch (error) {
navigate('/login');
} finally {
setLoading(false);
}
};
checkAuth();
}, [navigate]);
if (loading) {
return <div>Checking authentication...</div>;
}
if (!user) {
return null; // Will redirect
}
return <WrappedComponent {...props} user={user} />;
};
}
// Protected components
const AdminDashboard = withAuth(function AdminDashboard({ user }) {
return (
<div>
<h1>Admin Dashboard</h1>
<p>Welcome, {user.name}</p>
</div>
);
}, ['admin']);
const UserDashboard = withAuth(function UserDashboard({ user }) {
return (
<div>
<h1>User Dashboard</h1>
<p>Welcome, {user.name}</p>
</div>
);
}, ['user', 'admin']);
Data Fetching HOC
Generic Data Fetcher
function withData(WrappedComponent, dataSource) {
return function WithDataComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
// dataSource can be a function or string
const url = typeof dataSource === 'function'
? dataSource(props)
: dataSource;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [props.userId, props.refresh]); // Dependencies based on props
return (
<WrappedComponent
{...props}
data={data}
loading={loading}
error={error}
refetch={() => setData(null)} // Trigger refetch
/>
);
};
}
// Usage with different data sources
const UserProfileWithData = withData(
function UserProfile({ data: user, loading, error }) {
if (loading) return <div>Loading user...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
},
(props) => `/api/users/${props.userId}`
);
const PostListWithData = withData(
function PostList({ data: posts, loading, error, refetch }) {
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<button onClick={refetch}>Refresh</button>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
},
'/api/posts'
);
Theme HOC
Theming with HOC
const ThemeContext = createContext();
function withTheme(WrappedComponent) {
return function ThemedComponent(props) {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error('withTheme must be used within ThemeProvider');
}
return <WrappedComponent {...props} theme={theme} />;
};
}
// Theme provider
function ThemeProvider({ children }) {
const [currentTheme, setCurrentTheme] = useState('light');
const themes = {
light: {
background: '#ffffff',
text: '#333333',
primary: '#007bff'
},
dark: {
background: '#333333',
text: '#ffffff',
primary: '#0d6efd'
}
};
const value = {
...themes[currentTheme],
themeName: currentTheme,
toggleTheme: () => setCurrentTheme(prev => prev === 'light' ? 'dark' : 'light')
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Themed components
const ThemedButton = withTheme(function Button({ theme, children, ...props }) {
const style = {
backgroundColor: theme.primary,
color: theme.background,
border: 'none',
padding: '10px 20px',
borderRadius: '4px',
cursor: 'pointer'
};
return (
<button style={style} {...props}>
{children}
</button>
);
});
const ThemedCard = withTheme(function Card({ theme, children }) {
const style = {
backgroundColor: theme.background,
color: theme.text,
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
};
return <div style={style}>{children}</div>;
});
Performance HOC
Memoization HOC
function withMemo(WrappedComponent, areEqual) {
const MemoizedComponent = React.memo(WrappedComponent, areEqual);
// Preserve display name for debugging
MemoizedComponent.displayName = `withMemo(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
return MemoizedComponent;
}
// Usage
const ExpensiveComponent = withMemo(
function ExpensiveComponent({ data, filter }) {
console.log('ExpensiveComponent rendered');
const processedData = useMemo(() => {
return data
.filter(item => item.category === filter)
.sort((a, b) => b.score - a.score);
}, [data, filter]);
return (
<div>
{processedData.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
},
// Custom comparison function
(prevProps, nextProps) => {
return (
prevProps.data === nextProps.data &&
prevProps.filter === nextProps.filter
);
}
);
Error Boundary HOC
Error Handling with HOC
function withErrorBoundary(WrappedComponent, errorFallback) {
return class 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);
// Send to error reporting service
if (window.Sentry) {
window.Sentry.captureException(error);
}
}
render() {
if (this.state.hasError) {
if (errorFallback) {
return errorFallback(this.state.error, this.props);
}
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Something went wrong</h2>
<details style={{ marginTop: '10px' }}>
{this.state.error && this.state.error.toString()}
</details>
<button
onClick={() => this.setState({ hasError: false, error: null })}
style={{ marginTop: '10px' }}
>
Try Again
</button>
</div>
);
}
return <WrappedComponent {...this.props} />;
}
};
}
// Custom error fallback
const customErrorFallback = (error, props) => (
<div style={{ background: '#ffe6e6', padding: '20px', border: '1px solid #ff0000' }}>
<h3>Oops! Something went wrong</h3>
<p>Error in component: {props.componentName || 'Unknown'}</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
// Protected components
const SafeUserProfile = withErrorBoundary(
function UserProfile({ user }) {
// This might throw an error
if (!user) throw new Error('User data is required');
return <div>{user.name}</div>;
},
customErrorFallback
);
Composing HOCs
Combining Multiple HOCs
// Helper function to compose HOCs
function compose(...hocs) {
return (WrappedComponent) => {
return hocs.reduceRight((acc, hoc) => hoc(acc), WrappedComponent);
};
}
// Or using a library like recompose
// import { compose } from 'recompose';
// Individual HOCs
function withLoading(WrappedComponent) {
return function WithLoadingComponent(props) {
if (props.isLoading) return <div>Loading...</div>;
return <WrappedComponent {...props} />;
};
}
function withError(WrappedComponent) {
return function WithErrorComponent(props) {
if (props.error) return <div>Error: {props.error}</div>;
return <WrappedComponent {...props} />;
};
}
function withData(apiEndpoint) {
return function(WrappedComponent) {
return function WithDataComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(apiEndpoint)
.then(response => response.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
return (
<WrappedComponent
{...props}
data={data}
isLoading={loading}
error={error}
/>
);
};
};
}
// Compose multiple HOCs
const enhance = compose(
withData('/api/users'),
withLoading,
withError,
withAuth(['user', 'admin'])
);
const EnhancedUserList = enhance(function UserList({ data: users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
});
Advanced HOC Patterns
HOC with Configuration
function withAnalytics(config = {}) {
const {
trackClicks = true,
trackViews = true,
customEvents = []
} = config;
return function(WrappedComponent) {
return function AnalyticsComponent(props) {
const componentRef = useRef();
useEffect(() => {
if (trackViews) {
// Track component view
analytics.track('component_viewed', {
component: WrappedComponent.name,
props: Object.keys(props)
});
}
}, []);
const handleClick = useCallback((event) => {
if (trackClicks) {
analytics.track('component_clicked', {
component: WrappedComponent.name,
target: event.target.tagName
});
}
// Call original onClick if it exists
if (props.onClick) {
props.onClick(event);
}
}, [props.onClick]);
// Track custom events
const customEventHandlers = customEvents.reduce((handlers, eventName) => {
handlers[eventName] = (data) => {
analytics.track(eventName, {
component: WrappedComponent.name,
...data
});
};
return handlers;
}, {});
return (
<WrappedComponent
ref={componentRef}
{...props}
onClick={handleClick}
analytics={customEventHandlers}
/>
);
};
};
}
// Usage with configuration
const TrackedButton = withAnalytics({
trackClicks: true,
trackViews: false,
customEvents: ['button_hover', 'button_focus']
})(function Button({ children, analytics, ...props }) {
return (
<button
{...props}
onMouseEnter={() => analytics.button_hover?.()}
onFocus={() => analytics.button_focus?.()}
>
{children}
</button>
);
});
Dynamic HOC
function withFeatureToggle(featureName, fallbackComponent = null) {
return function(WrappedComponent) {
return function FeatureToggleComponent(props) {
const [isEnabled, setIsEnabled] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if feature is enabled
checkFeatureFlag(featureName)
.then(setIsEnabled)
.finally(() => setLoading(false));
}, []);
if (loading) {
return <div>Loading feature...</div>;
}
if (!isEnabled) {
if (fallbackComponent) {
return React.createElement(fallbackComponent, props);
}
return null;
}
return <WrappedComponent {...props} />;
};
};
}
// Usage
const FallbackComponent = () => <div>This feature is not available</div>;
const NewFeature = withFeatureToggle(
'new_dashboard',
FallbackComponent
)(function NewDashboard() {
return <div>New Dashboard Feature!</div>;
});
HOC Best Practices
Forwarding Refs
function withLogging(WrappedComponent) {
const WithLoggingComponent = React.forwardRef((props, ref) => {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`);
return () => console.log(`${WrappedComponent.name} unmounted`);
}, []);
return <WrappedComponent {...props} ref={ref} />;
});
// Set display name for debugging
WithLoggingComponent.displayName = `withLogging(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
return WithLoggingComponent;
}
Static Hoisting
import hoistNonReactStatics from 'hoist-non-react-statics';
function withEnhancement(WrappedComponent) {
function EnhancedComponent(props) {
return <WrappedComponent {...props} enhanced />;
}
// Copy static methods from WrappedComponent to EnhancedComponent
hoistNonReactStatics(EnhancedComponent, WrappedComponent);
return EnhancedComponent;
}
When to Use HOCs
Good Use Cases
- Cross-cutting concerns (auth, logging, analytics)
- Code reuse across multiple components
- Conditional rendering based on external state
- Adding lifecycle methods to functional components (before hooks)
When to Avoid
- Simple prop manipulation (use regular components)
- When hooks can solve the problem more elegantly
- Deep nesting of HOCs (prefer composition)
- Performance-critical components (hooks are often better)
HOCs vs Hooks
// HOC approach
const withCounter = (WrappedComponent) => {
return function WithCounterComponent(props) {
const [count, setCount] = useState(0);
return <WrappedComponent {...props} count={count} setCount={setCount} />;
};
};
// Hook approach (often preferred)
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
return [count, setCount];
}
HOCs are a powerful pattern for sharing logic between components. While hooks have replaced many HOC use cases, HOCs are still valuable for certain scenarios!