Skip
Arish's avatar

30. Higher Order Components


Higher Order Components (HOC)

A Higher Order Component is a function that takes a component and returns a new enhanced component.

Basic HOC Pattern

jsx
1// HOC that adds loading state
2function withLoading(WrappedComponent) {
3  return function WithLoadingComponent({ isLoading, ...props }) {
4    if (isLoading) {
5      return <div className="spinner">Loading...</div>
6    }
7    
8    return <WrappedComponent {...props} />
9  }
10}
11
12// Usage
13function UserList({ users }) {
14  return (
15    <ul>
16      {users.map(user => <li key={user.id}>{user.name}</li>)}
17    </ul>
18  )
19}
20
21const UserListWithLoading = withLoading(UserList)
22
23// In parent
24<UserListWithLoading isLoading={loading} users={users} />

HOC with Authentication

jsx
1function withAuth(WrappedComponent) {
2  return function WithAuthComponent(props) {
3    const { user, loading } = useAuth()
4    const navigate = useNavigate()
5    
6    useEffect(() => {
7      if (!loading && !user) {
8        navigate('/login')
9      }
10    }, [user, loading, navigate])
11    
12    if (loading) {
13      return <LoadingSpinner />
14    }
15    
16    if (!user) {
17      return null
18    }
19    
20    return <WrappedComponent {...props} user={user} />
21  }
22}
23
24// Usage
25function Dashboard({ user }) {
26  return <h1>Welcome, {user.name}!</h1>
27}
28
29const ProtectedDashboard = withAuth(Dashboard)

HOC with Data Fetching

jsx
1function withData(WrappedComponent, fetchData) {
2  return function WithDataComponent(props) {
3    const [data, setData] = useState(null)
4    const [loading, setLoading] = useState(true)
5    const [error, setError] = useState(null)
6    
7    useEffect(() => {
8      const load = async () => {
9        try {
10          const result = await fetchData(props)
11          setData(result)
12        } catch (err) {
13          setError(err.message)
14        } finally {
15          setLoading(false)
16        }
17      }
18      
19      load()
20    }, [])
21    
22    if (loading) return <Loading />
23    if (error) return <Error message={error} />
24    
25    return <WrappedComponent {...props} data={data} />
26  }
27}
28
29// Usage
30function UserProfile({ data }) {
31  return <div>{data.name}</div>
32}
33
34const UserProfileWithData = withData(
35  UserProfile,
36  (props) => fetch(`/api/users/${props.userId}`).then(r => r.json())
37)

HOC with Theme

jsx
1function withTheme(WrappedComponent) {
2  return function WithThemeComponent(props) {
3    const theme = useTheme()
4    
5    return <WrappedComponent {...props} theme={theme} />
6  }
7}
8
9// Or using forwardRef for refs
10function withTheme(WrappedComponent) {
11  const WithTheme = React.forwardRef((props, ref) => {
12    const theme = useTheme()
13    return <WrappedComponent {...props} ref={ref} theme={theme} />
14  })
15  
16  WithTheme.displayName = `WithTheme(${WrappedComponent.displayName || WrappedComponent.name})`
17  
18  return WithTheme
19}

Composing HOCs

jsx
1// Multiple HOCs
2const EnhancedComponent = withAuth(withTheme(withLoading(MyComponent)))
3
4// Or using a compose utility
5function compose(...fns) {
6  return (component) => fns.reduceRight((acc, fn) => fn(acc), component)
7}
8
9const enhance = compose(
10  withAuth,
11  withTheme,
12  withLoading
13)
14
15const EnhancedComponent = enhance(MyComponent)

HOC Best Practices

Pass Through Props

jsx
1function withHOC(WrappedComponent) {
2  return function HOCComponent(props) {
3    const additionalProps = { /* ... */ }
4    
5    // ✅ Pass all props through
6    return <WrappedComponent {...props} {...additionalProps} />
7  }
8}

Copy Static Methods

jsx
1import hoistNonReactStatics from 'hoist-non-react-statics'
2
3function withHOC(WrappedComponent) {
4  function HOCComponent(props) {
5    return <WrappedComponent {...props} />
6  }
7  
8  // Copy static methods
9  hoistNonReactStatics(HOCComponent, WrappedComponent)
10  
11  return HOCComponent
12}

Display Name for Debugging

jsx
1function withHOC(WrappedComponent) {
2  function HOCComponent(props) {
3    return <WrappedComponent {...props} />
4  }
5  
6  HOCComponent.displayName = `WithHOC(${
7    WrappedComponent.displayName || WrappedComponent.name || 'Component'
8  })`
9  
10  return HOCComponent
11}

HOC vs Hooks

Most HOC patterns can be replaced with hooks:

jsx
1// HOC
2const UserListWithLoading = withLoading(UserList)
3
4// Hook equivalent
5function UserList() {
6  const { data, loading } = useFetch('/api/users')
7  
8  if (loading) return <Loading />
9  
10  return <ul>{data.map(user => ...)}</ul>
11}

When to use HOCs

  • Wrapping with providers
  • Adding lifecycle methods
  • Injecting props from external sources
  • Creating reusable wrappers

When to use Hooks

  • Sharing stateful logic
  • Side effects
  • Context consumption
  • Most new code (preferred approach)

HOCs are a powerful pattern for component composition!