Higher-Order Components (HOCs) are functions that take a component and return a new enhanced component.
Basic HOC#
1// HOC that adds loading state
2function withLoading(WrappedComponent) {
3 return function WithLoadingComponent({ isLoading, ...props }) {
4 if (isLoading) {
5 return <div>Loading...</div>;
6 }
7 return <WrappedComponent {...props} />;
8 };
9}
10
11// Usage
12function UserList({ users }) {
13 return (
14 <ul>
15 {users.map(user => (
16 <li key={user.id}>{user.name}</li>
17 ))}
18 </ul>
19 );
20}
21
22const UserListWithLoading = withLoading(UserList);
23
24// In parent component
25<UserListWithLoading isLoading={loading} users={users} />HOC with Configuration#
1// HOC factory with options
2function withAuth(requiredRole) {
3 return function(WrappedComponent) {
4 return function AuthenticatedComponent(props) {
5 const { user, isAuthenticated } = useAuth();
6
7 if (!isAuthenticated) {
8 return <Redirect to="/login" />;
9 }
10
11 if (requiredRole && user.role !== requiredRole) {
12 return <Redirect to="/unauthorized" />;
13 }
14
15 return <WrappedComponent {...props} user={user} />;
16 };
17 };
18}
19
20// Usage
21const AdminDashboard = withAuth('admin')(Dashboard);
22const UserProfile = withAuth()(Profile);
23
24// Multiple configurations
25const withAdminAuth = withAuth('admin');
26const withUserAuth = withAuth('user');
27
28const AdminPanel = withAdminAuth(Panel);Injecting Props#
1// HOC that injects data
2function withUserData(WrappedComponent) {
3 return function WithUserDataComponent(props) {
4 const [user, setUser] = useState(null);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 fetchCurrentUser()
9 .then(setUser)
10 .finally(() => setLoading(false));
11 }, []);
12
13 if (loading) return <Spinner />;
14 if (!user) return <LoginPrompt />;
15
16 return <WrappedComponent {...props} currentUser={user} />;
17 };
18}
19
20// HOC that injects context
21function withTheme(WrappedComponent) {
22 return function WithThemeComponent(props) {
23 const theme = useContext(ThemeContext);
24 return <WrappedComponent {...props} theme={theme} />;
25 };
26}
27
28// Usage
29const ThemedButton = withTheme(Button);Display Name#
1// Set display name for debugging
2function withSubscription(WrappedComponent) {
3 function WithSubscription(props) {
4 // HOC logic
5 return <WrappedComponent {...props} />;
6 }
7
8 // Set display name
9 WithSubscription.displayName = `WithSubscription(${
10 WrappedComponent.displayName || WrappedComponent.name || 'Component'
11 })`;
12
13 return WithSubscription;
14}
15
16// Helper function
17function getDisplayName(WrappedComponent) {
18 return WrappedComponent.displayName || WrappedComponent.name || 'Component';
19}Forwarding Refs#
1// HOC that forwards refs
2function withLogger(WrappedComponent) {
3 const WithLogger = React.forwardRef((props, ref) => {
4 useEffect(() => {
5 console.log('Component mounted:', props);
6 }, []);
7
8 return <WrappedComponent ref={ref} {...props} />;
9 });
10
11 WithLogger.displayName = `WithLogger(${getDisplayName(WrappedComponent)})`;
12
13 return WithLogger;
14}
15
16// Usage
17const LoggedInput = withLogger(Input);
18
19function Form() {
20 const inputRef = useRef(null);
21
22 return <LoggedInput ref={inputRef} />;
23}Composing HOCs#
1// Multiple HOCs
2const EnhancedComponent = withRouter(
3 withAuth(
4 withTheme(
5 MyComponent
6 )
7 )
8);
9
10// Or compose them
11function compose(...funcs) {
12 if (funcs.length === 0) return arg => arg;
13 if (funcs.length === 1) return funcs[0];
14 return funcs.reduce((a, b) => (...args) => a(b(...args)));
15}
16
17const enhance = compose(
18 withRouter,
19 withAuth,
20 withTheme
21);
22
23const EnhancedComponent = enhance(MyComponent);
24
25// Named composition
26const withUserEnhancements = compose(
27 withAuth,
28 withUserData,
29 withErrorBoundary
30);
31
32const EnhancedProfile = withUserEnhancements(Profile);Static Methods#
1// Copy static methods
2import hoistNonReactStatics from 'hoist-non-react-statics';
3
4function withEnhancements(WrappedComponent) {
5 function Enhanced(props) {
6 return <WrappedComponent {...props} enhanced />;
7 }
8
9 // Copy static methods
10 hoistNonReactStatics(Enhanced, WrappedComponent);
11
12 return Enhanced;
13}
14
15// Original component with statics
16function DataGrid(props) {
17 return <div>Grid</div>;
18}
19
20DataGrid.defaultColumnWidth = 100;
21DataGrid.formatters = { date: formatDate };
22
23// Static methods are preserved
24const EnhancedGrid = withEnhancements(DataGrid);
25console.log(EnhancedGrid.defaultColumnWidth); // 100Error Boundary HOC#
1function withErrorBoundary(WrappedComponent, FallbackComponent) {
2 return class ErrorBoundary extends React.Component {
3 state = { hasError: false, error: null };
4
5 static getDerivedStateFromError(error) {
6 return { hasError: true, error };
7 }
8
9 componentDidCatch(error, errorInfo) {
10 console.error('Error caught:', error, errorInfo);
11 // Log to error service
12 }
13
14 render() {
15 if (this.state.hasError) {
16 if (FallbackComponent) {
17 return <FallbackComponent error={this.state.error} />;
18 }
19 return <div>Something went wrong.</div>;
20 }
21
22 return <WrappedComponent {...this.props} />;
23 }
24 };
25}
26
27// Usage
28const SafeComponent = withErrorBoundary(
29 RiskyComponent,
30 ({ error }) => <div>Error: {error.message}</div>
31);Data Fetching HOC#
1function withData(fetchData, mapDataToProps) {
2 return function(WrappedComponent) {
3 return function WithDataComponent(props) {
4 const [data, setData] = useState(null);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 setLoading(true);
10 fetchData(props)
11 .then(setData)
12 .catch(setError)
13 .finally(() => setLoading(false));
14 }, [props.id]); // Refetch when id changes
15
16 if (loading) return <Spinner />;
17 if (error) return <Error error={error} />;
18 if (!data) return null;
19
20 const dataProps = mapDataToProps ? mapDataToProps(data) : { data };
21
22 return <WrappedComponent {...props} {...dataProps} />;
23 };
24 };
25}
26
27// Usage
28const UserProfile = withData(
29 (props) => fetch(`/api/users/${props.id}`).then(r => r.json()),
30 (user) => ({ user })
31)(Profile);
32
33<UserProfile id={123} />Conditional Rendering HOC#
1// Show different component based on condition
2function branch(predicate, TrueComponent, FalseComponent = () => null) {
3 return function(WrappedComponent) {
4 return function BranchedComponent(props) {
5 if (predicate(props)) {
6 return <TrueComponent {...props} />;
7 }
8 return FalseComponent
9 ? <FalseComponent {...props} />
10 : <WrappedComponent {...props} />;
11 };
12 };
13}
14
15// Usage
16const MobileOrDesktop = branch(
17 props => props.isMobile,
18 MobileComponent,
19 DesktopComponent
20)(BaseComponent);
21
22// Or simpler
23function renderIf(predicate) {
24 return function(WrappedComponent) {
25 return function(props) {
26 if (!predicate(props)) return null;
27 return <WrappedComponent {...props} />;
28 };
29 };
30}
31
32const OnlyForAdmins = renderIf(props => props.user?.isAdmin)(AdminPanel);HOCs vs Hooks#
1// HOC approach
2function withWindowSize(WrappedComponent) {
3 return function(props) {
4 const [size, setSize] = useState({
5 width: window.innerWidth,
6 height: window.innerHeight
7 });
8
9 useEffect(() => {
10 const handleResize = () => {
11 setSize({
12 width: window.innerWidth,
13 height: window.innerHeight
14 });
15 };
16
17 window.addEventListener('resize', handleResize);
18 return () => window.removeEventListener('resize', handleResize);
19 }, []);
20
21 return <WrappedComponent {...props} windowSize={size} />;
22 };
23}
24
25// Hook approach (often preferred)
26function useWindowSize() {
27 const [size, setSize] = useState({
28 width: window.innerWidth,
29 height: window.innerHeight
30 });
31
32 useEffect(() => {
33 const handleResize = () => {
34 setSize({
35 width: window.innerWidth,
36 height: window.innerHeight
37 });
38 };
39
40 window.addEventListener('resize', handleResize);
41 return () => window.removeEventListener('resize', handleResize);
42 }, []);
43
44 return size;
45}
46
47// When to use HOCs:
48// - Class components
49// - Adding wrapper elements
50// - Component composition in libraries
51// - Cross-cutting concerns across many components
52
53// When to use hooks:
54// - Functional components
55// - Simpler mental model
56// - No wrapper component needed
57// - Better TypeScript supportBest Practices#
HOC Design:
✓ Pass through unrelated props
✓ Set displayName for debugging
✓ Forward refs when needed
✓ Copy static methods
Naming:
✓ Use 'with' prefix (withAuth, withData)
✓ Descriptive names
✓ Match injected prop names
Performance:
✓ Don't create HOCs inside render
✓ Memoize when appropriate
✓ Avoid unnecessary re-renders
Avoid:
✗ Mutating the original component
✗ Using HOCs inside render method
✗ HOC inside another HOC definition
✗ Over-nesting HOCs
Conclusion#
Higher-Order Components provide a powerful pattern for reusing component logic. They wrap components to inject props, handle authentication, add error boundaries, and more. While hooks have replaced many HOC use cases, HOCs remain valuable for class components and certain composition patterns. Use compose for combining multiple HOCs, remember to forward refs and copy static methods, and consider hooks as a simpler alternative for functional components.