Back to Blog
ReactHOCPatternsComponents

React Higher-Order Components Guide

Master React Higher-Order Components for reusable component logic.

B
Bootspring Team
Engineering
July 23, 2018
6 min read

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); // 100

Error 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 support

Best 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.

Share this article

Help spread the word about Bootspring