Before hooks existed, React developers used Higher-Order Components (HOCs) to share logic between components. An HOC is a function that takes a component and returns a new component with additional props or behavior. While custom hooks are now preferred for most use cases, HOCs are still used in many codebases and libraries (like Redux's connect and React Router's withRouter). Let's learn this classic React pattern! š
What Are Higher-Order Components?
An HOC is a function that takes a component and returns a new enhanced component:
// Basic HOC structure
function withSomething(Component) {
return function EnhancedComponent(props) {
// Add extra functionality here
const extraProp = 'some value'
// Return original component with extra props
return <Component {...props} extraProp={extraProp} />
}
}
// Original component
function MyComponent({ name, extraProp }) {
return (
<div>
<p>Name: {name}</p>
<p>Extra: {extraProp}</p>
</div>
)
}
// Enhanced component
const EnhancedMyComponent = withSomething(MyComponent)
// Usage
function App() {
return <EnhancedMyComponent name="Alice" />
}
// MyComponent receives both 'name' and 'extraProp'!HOC Definition:
A Higher-Order Component is:
⢠A function (not a component)
⢠Takes a component as input
⢠Returns a new component as output
⢠The new component wraps the original
⢠Adds props, behavior, or both
Pattern: HOC(Component) ā EnhancedComponent
Why Use HOCs?
HOCs solve the problem of code reuse without components knowing about each other:
The Problem: Repeated Logic
// ā Component 1: Repeated loading logic
function UserProfile() {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return <div>{user.name}</div>
}
// ā Component 2: Same loading logic!
function PostList() {
const [posts, setPosts] = React.useState([])
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
// Problem: Loading logic duplicated!The Solution: Extract to HOC
// ā
HOC that adds loading logic
function withLoading(Component, fetchData) {
return function WithLoadingComponent(props) {
const [data, setData] = React.useState(null)
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
fetchData()
.then(result => {
setData(result)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return <Component {...props} data={data} />
}
}
// ā
Simple components that receive data
function UserProfile({ data }) {
return <div>{data.name}</div>
}
function PostList({ data }) {
return <ul>{data.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
// ā
Enhance components with HOC
const UserProfileWithLoading = withLoading(
UserProfile,
() => fetch('/api/user').then(res => res.json())
)
const PostListWithLoading = withLoading(
PostList,
() => fetch('/api/posts').then(res => res.json())
)
// Loading logic written once, used many times!HOC Conventions
1. Naming: Start with "with"
// ā
Good - Clear HOC names
withAuth
withLoading
withLogger
withRouter
withTheme
// ā Bad - Unclear names
enhance
wrap
modify2. Pass Through Props
// ā
Good - Pass through all props
function withExtraData(Component) {
return function WithExtraData(props) {
const extraData = 'some data'
// Spread all original props + add new ones
return <Component {...props} extraData={extraData} />
}
}
// ā Bad - Props not passed through
function withExtraData(Component) {
return function WithExtraData(props) {
const extraData = 'some data'
// Original props lost!
return <Component extraData={extraData} />
}
}3. Don't Mutate Component
// ā Bad - Mutating original component
function withLogger(Component) {
Component.prototype.componentDidUpdate = function() {
console.log('Updated')
}
return Component // Mutation!
}
// ā
Good - Return new component
function withLogger(Component) {
return function WithLogger(props) {
React.useEffect(() => {
console.log('Rendered with props:', props)
})
return <Component {...props} />
}
}4. Set Display Name
function withAuth(Component) {
const WithAuth = (props) => {
// ... auth logic
return <Component {...props} />
}
// ā
Set displayName for debugging
WithAuth.displayName = `WithAuth(${Component.displayName || Component.name || 'Component'})`
return WithAuth
}
// Shows "WithAuth(UserProfile)" in React DevToolsCommon HOC Patterns
1. withAuth - Authentication Check
function withAuth(Component) {
return function WithAuth(props) {
const { user, loading } = useAuth() // Custom hook
if (loading) {
return <div>Loading...</div>
}
if (!user) {
// Redirect to login
return <Navigate to="/login" />
}
// User is authenticated, render component
return <Component {...props} user={user} />
}
}
// Usage:
function Dashboard({ user }) {
return <h1>Welcome, {user.name}!</h1>
}
const ProtectedDashboard = withAuth(Dashboard)
// Dashboard only renders if user is logged in2. withLoading - Loading State
function withLoading(Component) {
return function WithLoading({ isLoading, ...props }) {
if (isLoading) {
return (
<div className="loading">
<Spinner />
<p>Loading...</p>
</div>
)
}
return <Component {...props} />
}
}
// Usage:
function UserList({ users }) {
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
const UserListWithLoading = withLoading(UserList)
function App() {
const [users, setUsers] = React.useState([])
const [loading, setLoading] = React.useState(true)
// ... fetch users
return <UserListWithLoading users={users} isLoading={loading} />
}3. withLogger - Console Logging
function withLogger(Component) {
return function WithLogger(props) {
React.useEffect(() => {
console.log(`${Component.name} mounted with props:`, props)
return () => {
console.log(`${Component.name} unmounted`)
}
}, [props])
React.useEffect(() => {
console.log(`${Component.name} updated with props:`, props)
})
return <Component {...props} />
}
}
// Usage:
const UserProfileWithLogger = withLogger(UserProfile)
// Logs all renders and props changes4. withTheme - Theme Injection
// Theme context
const ThemeContext = React.createContext()
function withTheme(Component) {
return function WithTheme(props) {
const theme = React.useContext(ThemeContext)
return <Component {...props} theme={theme} />
}
}
// Usage:
function Button({ theme, children }) {
return (
<button style={{
backgroundColor: theme.primary,
color: theme.text
}}>
{children}
</button>
)
}
const ThemedButton = withTheme(Button)
// Button automatically gets theme from context5. withData - Data Fetching
function withData(Component, fetchData) {
return function WithData(props) {
const [data, setData] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
fetchData(props)
.then(result => {
setData(result)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [props])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return <Component {...props} data={data} />
}
}
// Usage:
function UserProfile({ data }) {
return <div>{data.name}</div>
}
const UserProfileWithData = withData(
UserProfile,
(props) => fetch(`/api/users/${props.userId}`).then(r => r.json())
)Interactive Demo
š” Tip: Edit the code above and click "Run" to see your changes
Composing Multiple HOCs
You can apply multiple HOCs to the same component:
// Multiple HOCs
function UserProfile({ user, theme }) {
return (
<div style={{ color: theme.text }}>
<h1>{user.name}</h1>
</div>
)
}
// ā Nested - Hard to read
const EnhancedUserProfile = withAuth(
withTheme(
withLogger(UserProfile)
)
)
// ā
Better - Use compose utility
function compose(...fns) {
return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
}
const enhance = compose(
withAuth,
withTheme,
withLogger
)
const EnhancedUserProfile = enhance(UserProfile)
// ā
Or apply one by one (clearest)
let EnhancedUserProfile = UserProfile
EnhancedUserProfile = withLogger(EnhancedUserProfile)
EnhancedUserProfile = withTheme(EnhancedUserProfile)
EnhancedUserProfile = withAuth(EnhancedUserProfile)
// All three approaches work!HOCs vs Custom Hooks
When to Use HOCs:
- Legacy codebases - Already using HOCs
- Library APIs - Redux connect, React Router withRouter
- Component wrapping - Need to wrap JSX
- Class components - Can't use hooks
When to Use Custom Hooks:
- New code - Hooks are the modern approach
- Logic reuse - Easier to understand
- Multiple values - Return many values easily
- Function components - Natural fit
Same Logic, Different Approaches:
// HOC Approach
function withWindowWidth(Component) {
return function WithWindowWidth(props) {
const [width, setWidth] = React.useState(window.innerWidth)
React.useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return <Component {...props} windowWidth={width} />
}
}
const MyComponentWithWidth = withWindowWidth(MyComponent)
// Custom Hook Approach (Better!)
function useWindowWidth() {
const [width, setWidth] = React.useState(window.innerWidth)
React.useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return width
}
function MyComponent() {
const width = useWindowWidth()
return <div>Width: {width}</div>
}
// Hook version is simpler and more flexible!Modern React Recommendation:
For new code, prefer custom hooks over HOCs.
Hooks are:
⢠Easier to understand
⢠More flexible
⢠Less nesting/wrapping
⢠Better TypeScript support
⢠Composable without wrapper hell
But still learn HOCs for:
⢠Reading existing code
⢠Using libraries that provide HOCs
⢠Maintaining legacy projects
Common HOC Mistakes
1. Using HOCs Inside Render
// ā Wrong - Creates new component every render!
function MyComponent() {
const EnhancedComponent = withAuth(SomeComponent)
return <EnhancedComponent />
}
// Problem: EnhancedComponent is recreated every render
// React sees it as a different component and remounts it
// ā
Correct - Create outside component
const EnhancedComponent = withAuth(SomeComponent)
function MyComponent() {
return <EnhancedComponent />
}2. Not Copying Static Methods
// Original component with static method
function MyComponent() {
return <div>Component</div>
}
MyComponent.staticMethod = function() {
console.log('Static method')
}
// ā Wrong - Static method lost!
function withEnhancement(Component) {
return function Enhanced(props) {
return <Component {...props} />
}
}
const Enhanced = withEnhancement(MyComponent)
Enhanced.staticMethod() // Error! Undefined
// ā
Correct - Copy static methods
function withEnhancement(Component) {
function Enhanced(props) {
return <Component {...props} />
}
// Copy static methods
Enhanced.staticMethod = Component.staticMethod
return Enhanced
}
// ā
Or use library like hoist-non-react-statics
import hoistNonReactStatics from 'hoist-non-react-statics'
function withEnhancement(Component) {
function Enhanced(props) {
return <Component {...props} />
}
return hoistNonReactStatics(Enhanced, Component)
}3. Prop Name Collisions
// ā Wrong - HOC adds prop that component already uses
function withData(Component) {
return function WithData(props) {
const data = fetchData() // HOC's data
return <Component {...props} data={data} />
}
}
function MyComponent({ data }) {
// Which data? HOC's or parent's?
return <div>{data}</div>
}
// ā
Better - Use unique prop names
function withData(Component) {
return function WithData(props) {
const fetchedData = fetchData()
return <Component {...props} fetchedData={fetchedData} />
}
}
function MyComponent({ data, fetchedData }) {
// Clear which is which
return <div>{data} {fetchedData}</div>
}Key Takeaways
- HOCs are functions that take a component and return an enhanced component
- Pattern:
HOC(Component) ā EnhancedComponent - Name HOCs with "with" prefix (withAuth, withTheme)
- Always pass through original props with spread operator
- Don't mutate the original component
- Set displayName for better debugging
- Common patterns: auth, loading, logging, theme, data fetching
- Can compose multiple HOCs together
- Don't create HOCs inside render method
- Copy static methods to enhanced component
- Avoid prop name collisions
- Modern React prefers custom hooks over HOCs
- Still useful for legacy code and certain libraries
What's Next?
You now understand Higher-Order Components and when to use them! You know how to enhance components with reusable logic using the HOC pattern.
In the next lesson, you'll learn about Render Props Pattern:
- What are render props
- Using render props for code sharing
- Render props vs HOCs vs hooks
- Common render props patterns
- Best practices and pitfalls
Get ready to learn the final classic React pattern! šÆ