
Introduction
Higher Order Components (HOCs) are an advanced pattern in React that emerges from React’s compositional nature. They are not part of the React API but rather a design pattern that leverages React’s component model.
What is a Higher Order Components?
A Higher Order Component is a function that takes a component and returns a new component. It’s similar to higher-order functions in JavaScript, which take functions as arguments and/or return functions.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
HOCs are a way to reuse component logic. While React components transform props into UI, higher-order components transform a component into another component.
Why Use Higher Order Components?
HOCs allow you to:
- Abstract and reuse component logic
- Add additional props to components
- Override or extend existing behavior
- Apply cross-cutting concerns like logging, authentication, or data fetching
In essence, HOCs help you avoid duplicating code across your React application and promote better separation of concerns.
Understanding the HOC Pattern
Let’s break down the HOC pattern by examining its structure and behavior.
Core Concepts
- Pure Function: An HOC is a pure function with zero side effects.
- Doesn’t Modify the Input: HOCs don’t modify the wrapped component; they create a new one.
- Composition: HOCs compose the original component by wrapping it in a container component.
The HOC Signature
The typical signature of an HOC looks like this:
function withFeature(WrappedComponent) {
 return class extends React.Component {
  // New functionality here
 Â
  render() {
   // Pass all props to the wrapped component
   return <WrappedComponent {...this.props} />;
  }
 };
}
This pattern creates a new component that renders the wrapped component, potentially adding props or behavior.
Creating Your First HOC
Let’s create a simple HOC that adds a loading state to any component:
// withLoading.js
import React, { useState, useEffect } from 'react';
function withLoading(WrappedComponent, loadingMessage = 'Loading...') {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div className="loading-indicator">{loadingMessage}</div>;
}
return <WrappedComponent {...props} />;
};
}
export default withLoading;
Now let’s use this HOC with a component:
// UserList.js
import React from 'react';
import withLoading from './withLoading';
function UserList({ users }) {
 return (
  <ul>
   {users.map(user => (
    <li key={user.id}>{user.name}</li>
   ))}
  </ul>
 );
}
// Enhanced component with loading functionality
export default withLoading(UserList);
When using the enhanced component:
// App.js
import React, { useState, useEffect } from 'react';
import UserList from './UserList';
function App() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simulate API call
setTimeout(() => {
setUsers([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]);
setIsLoading(false);
}, 2000);
}, []);
return <UserList isLoading={isLoading} users={users} />;
}
This HOC effectively abstracts the loading logic away from our components, making them more focused on their specific responsibilities.
Common Use Cases for HOCs
Higher-Order Components are versatile and can be used in many scenarios:
1. Authentication and Authorization
function withAuth(WrappedComponent) {
 return function WithAuth(props) {
  const { isAuthenticated, user } = useAuth(); // Custom hook for auth
 Â
  if (!isAuthenticated) {
   return <Navigate to="/login" />;
  }
 Â
  return <WrappedComponent {...props} user={user} />;
 };
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
2. Data Fetching
function withData(WrappedComponent, fetchData) {
return function WithData(props) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchDataAndUpdate = async () => {
try {
setIsLoading(true);
const result = await fetchData();
setData(result);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
fetchDataAndUpdate();
}, []);
return (
<WrappedComponent
data={data}
isLoading={isLoading}
error={error}
{...props}
/>
);
};
}
// Usage
const fetchUsers = () => fetch('/api/users').then(res => res.json());
const UsersWithData = withData(UserList, fetchUsers);
3. Styling and Theming
function withTheme(WrappedComponent) {
 return function WithTheme(props) {
  const theme = useContext(ThemeContext);
 Â
  return <WrappedComponent theme={theme} {...props} />;
 };
}
// Usage
const ThemedButton = withTheme(Button);
4. Logging and Analytics
function withTracking(WrappedComponent, componentName) {
return function WithTracking(props) {
useEffect(() => {
// Log component mount
analytics.logEvent(`${componentName}_mounted`);
return () => {
// Log component unmount
analytics.logEvent(`${componentName}_unmounted`);
};
}, []);
const trackEvent = (eventName, data) => {
analytics.logEvent(`${componentName}_${eventName}`, data);
};
return <WrappedComponent {...props} trackEvent={trackEvent} />;
};
}
// Usage
const TrackedUserList = withTracking(UserList, 'UserList');
Best Practices and Conventions
When working with HOCs, follow these best practices to avoid common pitfalls:
1. Use Descriptive Display Names
To aid debugging, set a descriptive display name that identifies the HOC:
function withAuth(WrappedComponent) {
 function WithAuth(props) {
  // ... implementation
 }
Â
 WithAuth.displayName = `WithAuth(${
  WrappedComponent.displayName || WrappedComponent.name || 'Component'
 })`;
Â
 return WithAuth;
}
2. Don’t Mutate the Original Component
Create a new component instead of modifying the input component:
// Bad - mutating the original component
function badHOC(WrappedComponent) {
WrappedComponent.prototype.componentDidMount = function() {
// Do something
};
return WrappedComponent;
}
// Good - creating a new component
function goodHOC(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
// Do something
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
3. Pass Unrelated Props Through
Make sure your HOC passes through props that are unrelated to its specific concern:
function withAuth(WrappedComponent) {
return function WithAuth(props) {
const { isAuthenticated, user } = useAuth();
// Pass all props to the wrapped component, plus the user if authenticated
return isAuthenticated ? (
<WrappedComponent {...props} user={user} />
) : (
<Navigate to="/login" />
);
};
}
4. Maximize Composability
Design HOCs to be composable with other HOCs:
// Compose multiple HOCs
const EnhancedComponent = withAuth(withData(withTheme(MyComponent)));
// Or using a compose utility
import { compose } from 'redux';
const enhance = compose(
withAuth,
withData,
withTheme
);
const EnhancedComponent = enhance(MyComponent);
5. Use the Spread Operator for Props
Always use the spread operator to pass all props to the wrapped component:
function withExtraProps(WrappedComponent) {
 return function WithExtraProps(props) {
  const extraProp = 'This is an extra prop';
 Â
  // Pass all original props plus the extra one
  return <WrappedComponent extraProp={extraProp} {...props} />;
 };
}
Composing Multiple HOCs
When your application grows, you might need to apply multiple HOCs to a single component. Let’s explore how to compose HOCs effectively.
Manual Composition
You can compose HOCs manually by nesting the function calls:
const EnhancedComponent = withAuth(withData(withTheme(BaseComponent)));
However, this can become hard to read with many HOCs.
Using Composition Utilities
Libraries like Redux provide a compose utility to make this more readable:
import { compose } from 'redux';
const enhance = compose(
withAuth,
withData,
withTheme
);
const EnhancedComponent = enhance(BaseComponent);
You can also create your own compose function:
function compose(...funcs) {
 if (funcs.length === 0) {
  return arg => arg;
 }
Â
 if (funcs.length === 1) {
  return funcs[0];
 }
Â
 return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
Understanding the Order of Composition
It’s important to understand that composition happens from right to left. In the example:
const EnhancedComponent = withAuth(withData(withTheme(BaseComponent)));
- First, withTheme is applied to BaseComponent
- Then withData is applied to the result of withTheme(BaseComponent)
- Finally, withAuth is applied to the result of withData(withTheme(BaseComponent))
This order matters especially when HOCs depend on each other or modify similar props.
HOCs vs. Hooks
With the introduction of React Hooks in React 16.8, many use cases for HOCs can now be solved more elegantly with custom hooks. Let’s compare the two approaches:
HOC Approach
// HOC for adding theme
function withTheme(WrappedComponent) {
return function WithTheme(props) {
const theme = useContext(ThemeContext);
return <WrappedComponent theme={theme} {...props} />;
};
}
// Usage
const ThemedButton = withTheme(Button);
function App() {
return <ThemedButton onClick={() => alert('Clicked')} />;
}
Hook Approach
// Custom hook for theme
function useTheme() {
return useContext(ThemeContext);
}
// Usage
function Button(props) {
const theme = useTheme();
return <button style={{ color: theme.primary }} {...props} />;
}
function App() {
return <Button onClick={() => alert('Clicked')} />;
}
When to Use HOCs vs. Hooks
Use HOCs when:
- You need to share the exact same component logic across many components
- You want to inject props or modify the component tree
- You’re working with class components
- You need to override lifecycle methods
Use Hooks when:
- You want to reuse stateful logic, not the entire component
- You want to avoid the “wrapper hell” that can happen with nested HOCs
- You prefer function components
- You want more direct and readable code
In modern React applications, hooks are generally preferred for their simplicity and flexibility, but HOCs still have their place, especially in larger applications or when working with legacy code.
Debugging HOCs
Debugging components wrapped in multiple HOCs can be challenging. Here are some strategies to make debugging easier:
1. Use React DevTools
React DevTools can help you inspect the component hierarchy. When using HOCs, the component hierarchy becomes deeper, which can make debugging harder. Using proper display names helps identify components in the DevTools.
2. Static Methods and refs
HOCs can obscure static methods and refs of the wrapped component. To solve this:
- For static methods, either hoist them explicitly or use the hoist-non-react-statics library:
import hoistNonReactStatics from 'hoist-non-react-statics';
function withAuth(WrappedComponent) {
function WithAuth(props) {
// ... implementation
}
return hoistNonReactStatics(WithAuth, WrappedComponent);
}
- For refs, use the React.forwardRef API:
function withAuth(WrappedComponent) {
const WithAuth = React.forwardRef((props, ref) => {
// ... implementation
return <WrappedComponent ref={ref} {...props} />;
});
WithAuth.displayName = `WithAuth(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
return WithAuth;
}
3. Debugging Props
Add logging to see which props are being passed at each level:
function withLogging(WrappedComponent) {
 return function WithLogging(props) {
  console.log(`[WithLogging] Props:`, props);
  return <WrappedComponent {...props} />;
 };
}
Real-World Examples
Let’s examine some real-world examples of HOCs used in popular React libraries:
1. Redux’s connect
Redux’s connect function is a HOC that connects a React component to the Redux store:
import { connect } from 'react-redux';
function mapStateToProps(state) {
return {
todos: state.todos
};
}
function mapDispatchToProps(dispatch) {
return {
addTodo: text => dispatch({ type: 'ADD_TODO', text })
};
}
// TodoList is wrapped with the connect HOC
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
2. React Router’s withRouter
React Router’s withRouter HOC injects router props into your component:
import { withRouter } from 'react-router-dom';
function UserProfile({ match, history, location }) {
 const userId = match.params.id;
Â
 return (
  <div>
   <h1>User Profile for ID: {userId}</h1>
   <button onClick={() => history.push('/users')}>Back to Users</button>
  </div>
 );
}
export default withRouter(UserProfile);
Note: In newer versions of React Router (v6+), this HOC has been replaced with hooks.
3. Recompose Library
Before hooks, the Recompose library provided a collection of HOCs for common use cases:
import { compose, withState, withHandlers } from 'recompose';
const enhance = compose(
 withState('count', 'setCount', 0),
 withHandlers({
  increment: ({ count, setCount }) => () => setCount(count + 1),
  decrement: ({ count, setCount }) => () => setCount(count - 1)
 })
);
function Counter({ count, increment, decrement }) {
 return (
  <div>
   <p>Count: {count}</p>
   <button onClick={increment}>+</button>
   <button onClick={decrement}>-</button>
  </div>
 );
}
export default enhance(Counter);
Summary
Higher-Order Components are a powerful pattern for code reuse in React applications. Key takeaways from this chapter include:
- HOCs are functions that take a component and return a new enhanced component
- They allow you to abstract and reuse component logic across your application
- Common use cases include authentication, data fetching, styling, and analytics
- Best practices include using descriptive names, not mutating the original component, and passing unrelated props through
- HOCs can be composed to apply multiple enhancements to a single component
- In modern React development, hooks often provide a simpler alternative to HOCs, but both have their place
Understanding HOCs is essential for working with many React libraries and for creating maintainable, reusable code in complex React applications.
Exercises
- Basic HOC Implementation
- Create a withLogger HOC that logs when a component mounts and unmounts
- Apply it to a simple component and verify the logs in the console
- HOC with Configuration
- Create a withDelay HOC that renders a component after a specified delay
- Make the delay time configurable through parameters
- Composing HOCs
- Create three different HOCs: withLoading, withError, and withDataFetching
- Compose them together to create a component that handles data fetching, loading states, and error states
- Converting HOCs to Hooks
- Take one of your HOCs from the previous exercises
- Implement the same functionality using a custom hook
- Compare the two approaches and note the differences
- Real-World Application
- Create a small app with protected routes using a withAuth HOC
- Add analytics tracking with a withTracking HOC
- Apply both HOCs to your components and ensure they work correctly together
Additional Resources
Documentation
Libraries
- Recompose – A React utility belt for function components and HOCs
- hoist-non-react-statics – Utility for hoisting non-React statics in HOCs
Articles and Tutorials
- Top React Component Libraries: Supercharge Your Development in 2024
- What are React Server Components? A Guide for Modern Web Apps
- What are React Hooks?: A Comprehensive Guide
GitHub Repositories
- React HOC Examples – Collection of HOC examples and patterns
- React Patterns – Various React patterns including HOCs