What is a Higher-Order Component?
A Higher-Order Component (HOC) is a function that takes a component and returns a new component with additional props or functionality. It's like a component factory that enhances existing components.
Basic HOC Structure
// Basic HOC pattern
function withEnhancement(WrappedComponent) {
// Return a new component
return function EnhancedComponent(props) {
// Add new functionality here
const additionalProps = {
enhanced: true
};
// Render the wrapped component with combined props
return <WrappedComponent {...props} {...additionalProps} />;
};
}
// Usage
const EnhancedButton = withEnhancement(Button);
Common HOC Patterns
1. Adding Props
// HOC that adds loading functionality
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div className="loading">Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// Original component
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Enhanced component
const UserListWithLoading = withLoading(UserList);
// Usage
function App() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
return (
<UserListWithLoading
users={users}
isLoading={isLoading}
/>
);
}
2. Adding State
// HOC that adds toggle functionality
function withToggle(WrappedComponent) {
return function WithToggleComponent(props) {
const [isToggled, setIsToggled] = useState(false);
const toggle = () => setIsToggled(prev => !prev);
return (
<WrappedComponent
{...props}
isToggled={isToggled}
toggle={toggle}
/>
);
};
}
// Component that uses toggle
function ToggleablePanel({ isToggled, toggle, title, children }) {
return (
<div className="panel">
<button onClick={toggle}>
{title} {isToggled ? '▼' : '▶'}
</button>
{isToggled && <div className="panel-content">{children}</div>}
</div>
);
}
// Enhanced component
const Panel = withToggle(ToggleablePanel);
// Usage
<Panel title="Click to expand">
Hidden content here!
</Panel>
3. Adding Side Effects
// HOC that adds logging
function withLogging(WrappedComponent) {
return function WithLoggingComponent(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`);
return () => {
console.log(`${WrappedComponent.name} unmounted`);
};
}, []);
useEffect(() => {
console.log(`${WrappedComponent.name} updated`, props);
});
return <WrappedComponent {...props} />;
};
}
// HOC that adds data fetching
function withDataFetching(url) {
return function(WrappedComponent) {
return function WithDataFetchingComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, []);
return (
<WrappedComponent
{...props}
data={data}
loading={loading}
error={error}
/>
);
};
};
}
// Usage
const UserListWithData = withDataFetching('/api/users')(UserList);
const UserListWithLogging = withLogging(UserListWithData);
Advanced HOC Patterns
1. Configurable HOCs
// HOC with configuration options
function withTheme(options = {}) {
const { defaultTheme = 'light' } = options;
return function(WrappedComponent) {
return function WithThemeComponent(props) {
const [theme, setTheme] = useState(defaultTheme);
const toggleTheme = () => {
setTheme(current =>
current === 'light' ? 'dark' : 'light'
);
};
return (
<div className={`theme-${theme}`}>
<WrappedComponent
{...props}
theme={theme}
toggleTheme={toggleTheme}
/>
</div>
);
};
};
}
// Usage with configuration
const ThemedComponent = withTheme({
defaultTheme: 'dark'
})(MyComponent);
2. HOC Composition
// Multiple HOCs can be composed
function compose(...hocs) {
return function(BaseComponent) {
return hocs.reduceRight((acc, hoc) => {
return hoc(acc);
}, BaseComponent);
};
}
// Example HOCs
function withAuth(WrappedComponent) {
return function WithAuthComponent(props) {
const [user, setUser] = useState(null);
// Auth logic here...
return <WrappedComponent {...props} user={user} />;
};
}
function withAnalytics(WrappedComponent) {
return function WithAnalyticsComponent(props) {
useEffect(() => {
// Track page view
analytics.pageView(WrappedComponent.name);
}, []);
return <WrappedComponent {...props} />;
};
}
function withErrorBoundary(WrappedComponent) {
return class WithErrorBoundaryComponent extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <ErrorDisplay error={this.state.error} />;
}
return <WrappedComponent {...this.props} />;
}
};
}
// Compose multiple HOCs
const EnhancedComponent = compose(
withErrorBoundary,
withAuth,
withAnalytics,
withLogging
)(BaseComponent);
// Or use traditional nesting
const EnhancedComponent =
withErrorBoundary(
withAuth(
withAnalytics(
withLogging(BaseComponent)
)
)
);
Real-World HOC Examples
1. Authentication HOC
function withAuthentication(WrappedComponent) {
return function WithAuthenticationComponent(props) {
const { user, loading } = useAuth(); // Custom hook
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to="/login" />;
}
if (props.requiredRole && user.role !== props.requiredRole) {
return <AccessDenied />;
}
return <WrappedComponent {...props} user={user} />;
};
}
// Protected component
function AdminDashboard({ user }) {
return (
<div>
<h1>Admin Dashboard</h1>
<p>Welcome, {user.name}!</p>
{/* Admin features */}
</div>
);
}
// Secure the component
const SecureAdminDashboard = withAuthentication(AdminDashboard);
// Usage
<SecureAdminDashboard requiredRole="admin" />
2. Form Handling HOC
function withFormHandling(initialValues) {
return function(WrappedComponent) {
return function WithFormHandlingComponent(props) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
await props.onSubmit(values);
} catch (error) {
setErrors(error.validationErrors || {});
} finally {
setIsSubmitting(false);
}
};
return (
<WrappedComponent
{...props}
values={values}
errors={errors}
isSubmitting={isSubmitting}
handleChange={handleChange}
handleSubmit={handleSubmit}
/>
);
};
};
}
// Form component
function LoginForm({
values,
errors,
isSubmitting,
handleChange,
handleSubmit
}) {
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span>{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span>{errors.password}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
// Enhanced form
const EnhancedLoginForm = withFormHandling({
email: '',
password: ''
})(LoginForm);
3. Performance Monitoring HOC
function withPerformanceMonitoring(WrappedComponent) {
return function WithPerformanceComponent(props) {
const renderStartTime = useRef(performance.now());
useEffect(() => {
const renderEndTime = performance.now();
const renderTime = renderEndTime - renderStartTime.current;
console.log(
`${WrappedComponent.name} render time: ${renderTime}ms`
);
// Send to analytics
analytics.trackPerformance({
component: WrappedComponent.name,
renderTime,
props: Object.keys(props)
});
});
return <WrappedComponent {...props} />;
};
}
// Monitor component performance
const MonitoredDataGrid = withPerformanceMonitoring(DataGrid);
HOC Best Practices
1. Always Pass Props Through
// ❌ Bad - losing props
function withBadHOC(WrappedComponent) {
return function(props) {
// Not passing all props
return <WrappedComponent someProp={props.someProp} />;
};
}
// ✅ Good - preserving props
function withGoodHOC(WrappedComponent) {
return function(props) {
const enhancedProp = 'enhanced';
return (
<WrappedComponent
{...props}
enhanced={enhancedProp}
/>
);
};
}
2. Copy Static Methods
import hoistNonReactStatics from 'hoist-non-react-statics';
function withStaticCopy(WrappedComponent) {
function WithStaticComponent(props) {
// HOC logic
return <WrappedComponent {...props} />;
}
// Copy non-React static methods
hoistNonReactStatics(WithStaticComponent, WrappedComponent);
return WithStaticComponent;
}
// Original component with static method
class MyComponent extends React.Component {
static fetchData() {
// Fetch data
}
render() {
return <div>My Component</div>;
}
}
// Enhanced component preserves static methods
const Enhanced = withStaticCopy(MyComponent);
Enhanced.fetchData(); // Works!
3. Set Display Name
function withDisplayName(WrappedComponent) {
function WithDisplayNameComponent(props) {
return <WrappedComponent {...props} />;
}
// Set display name for debugging
const wrappedName = WrappedComponent.displayName
|| WrappedComponent.name
|| 'Component';
WithDisplayNameComponent.displayName =
`withDisplayName(${wrappedName})`;
return WithDisplayNameComponent;
}
HOC vs Other Patterns
When to Use HOCs vs Hooks
// HOC approach
function withWindowSize(WrappedComponent) {
return function WithWindowSizeComponent(props) {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <WrappedComponent {...props} windowSize={size} />;
};
}
// Hook approach (modern, preferred)
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Using the hook
function ComponentWithHook() {
const windowSize = useWindowSize();
return <div>Width: {windowSize.width}</div>;
}
Common HOC Pitfalls
1. HOCs Inside Render
// ❌ Bad - creates new component on every render
function ParentComponent() {
return (
<div>
{React.createElement(withLoading(MyComponent), props)}
</div>
);
}
// ✅ Good - create enhanced component once
const MyComponentWithLoading = withLoading(MyComponent);
function ParentComponent() {
return (
<div>
<MyComponentWithLoading {...props} />
</div>
);
}
2. Mutating the Original Component
// ❌ Bad - mutating the original component
function withMutation(WrappedComponent) {
WrappedComponent.prototype.newMethod = function() {
// Don't do this!
};
return WrappedComponent;
}
// ✅ Good - create a new component
function withEnhancement(WrappedComponent) {
return class extends React.Component {
newMethod() {
// Safe method
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Modern Alternatives to HOCs
While HOCs are still useful, many use cases are now better served by:
- Custom Hooks: For reusable logic
- Render Props: For dynamic rendering
- Context API: For dependency injection
- Component Composition: For UI structure
// Modern alternative using hooks
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Auth logic here
return { user, loading };
}
function SecureComponent() {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;
return <ProtectedContent user={user} />;
}