Higher-Order Components (HOCs) in React

Advanced Pattern for Component Reusability

Overview

Welcome to the world of Higher-Order Components! Just as a coffee shop can transform a simple espresso into various drinks by adding different ingredients, HOCs transform components by adding extra functionality. Today, you'll learn this powerful pattern that allows you to reuse component logic across your application.

What is a Higher-Order Component?

A Higher-Order Component (HOC) is a function that takes a component and returns a new component with additional props or functionality. It's like a component factory that enhances existing components.

graph LR A[Original Component] --> B[HOC Function] B --> C[Enhanced Component] D[Props] --> A E[Additional Props] --> C F[Original Props] --> C style B fill:#f9f,stroke:#333,stroke-width:2px style C fill:#9cf,stroke:#333,stroke-width:2px

Basic HOC Structure


// Basic HOC pattern
function withEnhancement(WrappedComponent) {
    // Return a new component
    return function EnhancedComponent(props) {
        // Add new functionality here
        const additionalProps = {
            enhanced: true
        };

        // Render the wrapped component with combined props
        return <WrappedComponent {...props} {...additionalProps} />;
    };
}

// Usage
const EnhancedButton = withEnhancement(Button);
            

Common HOC Patterns

1. Adding Props


// HOC that adds loading functionality
function withLoading(WrappedComponent) {
    return function WithLoadingComponent({ isLoading, ...props }) {
        if (isLoading) {
            return <div className="loading">Loading...</div>;
        }
        return <WrappedComponent {...props} />;
    };
}

// Original component
function UserList({ users }) {
    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}

// Enhanced component
const UserListWithLoading = withLoading(UserList);

// Usage
function App() {
    const [users, setUsers] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    return (
        <UserListWithLoading 
            users={users} 
            isLoading={isLoading} 
        />
    );
}
            

2. Adding State


// HOC that adds toggle functionality
function withToggle(WrappedComponent) {
    return function WithToggleComponent(props) {
        const [isToggled, setIsToggled] = useState(false);
        
        const toggle = () => setIsToggled(prev => !prev);
        
        return (
            <WrappedComponent 
                {...props} 
                isToggled={isToggled} 
                toggle={toggle} 
            />
        );
    };
}

// Component that uses toggle
function ToggleablePanel({ isToggled, toggle, title, children }) {
    return (
        <div className="panel">
            <button onClick={toggle}>
                {title} {isToggled ? '▼' : '▶'}
            </button>
            {isToggled && <div className="panel-content">{children}</div>}
        </div>
    );
}

// Enhanced component
const Panel = withToggle(ToggleablePanel);

// Usage
<Panel title="Click to expand">
    Hidden content here!
</Panel>
            

3. Adding Side Effects


// HOC that adds logging
function withLogging(WrappedComponent) {
    return function WithLoggingComponent(props) {
        useEffect(() => {
            console.log(`${WrappedComponent.name} mounted`);
            
            return () => {
                console.log(`${WrappedComponent.name} unmounted`);
            };
        }, []);
        
        useEffect(() => {
            console.log(`${WrappedComponent.name} updated`, props);
        });
        
        return <WrappedComponent {...props} />;
    };
}

// HOC that adds data fetching
function withDataFetching(url) {
    return function(WrappedComponent) {
        return function WithDataFetchingComponent(props) {
            const [data, setData] = useState(null);
            const [loading, setLoading] = useState(true);
            const [error, setError] = useState(null);
            
            useEffect(() => {
                fetch(url)
                    .then(response => response.json())
                    .then(data => {
                        setData(data);
                        setLoading(false);
                    })
                    .catch(error => {
                        setError(error);
                        setLoading(false);
                    });
            }, []);
            
            return (
                <WrappedComponent 
                    {...props} 
                    data={data} 
                    loading={loading} 
                    error={error} 
                />
            );
        };
    };
}

// Usage
const UserListWithData = withDataFetching('/api/users')(UserList);
const UserListWithLogging = withLogging(UserListWithData);
            

Advanced HOC Patterns

1. Configurable HOCs


// HOC with configuration options
function withTheme(options = {}) {
    const { defaultTheme = 'light' } = options;
    
    return function(WrappedComponent) {
        return function WithThemeComponent(props) {
            const [theme, setTheme] = useState(defaultTheme);
            
            const toggleTheme = () => {
                setTheme(current => 
                    current === 'light' ? 'dark' : 'light'
                );
            };
            
            return (
                <div className={`theme-${theme}`}>
                    <WrappedComponent 
                        {...props} 
                        theme={theme} 
                        toggleTheme={toggleTheme} 
                    />
                </div>
            );
        };
    };
}

// Usage with configuration
const ThemedComponent = withTheme({ 
    defaultTheme: 'dark' 
})(MyComponent);
            

2. HOC Composition


// Multiple HOCs can be composed
function compose(...hocs) {
    return function(BaseComponent) {
        return hocs.reduceRight((acc, hoc) => {
            return hoc(acc);
        }, BaseComponent);
    };
}

// Example HOCs
function withAuth(WrappedComponent) {
    return function WithAuthComponent(props) {
        const [user, setUser] = useState(null);
        
        // Auth logic here...
        
        return <WrappedComponent {...props} user={user} />;
    };
}

function withAnalytics(WrappedComponent) {
    return function WithAnalyticsComponent(props) {
        useEffect(() => {
            // Track page view
            analytics.pageView(WrappedComponent.name);
        }, []);
        
        return <WrappedComponent {...props} />;
    };
}

function withErrorBoundary(WrappedComponent) {
    return class WithErrorBoundaryComponent extends React.Component {
        state = { hasError: false, error: null };
        
        static getDerivedStateFromError(error) {
            return { hasError: true, error };
        }
        
        render() {
            if (this.state.hasError) {
                return <ErrorDisplay error={this.state.error} />;
            }
            
            return <WrappedComponent {...this.props} />;
        }
    };
}

// Compose multiple HOCs
const EnhancedComponent = compose(
    withErrorBoundary,
    withAuth,
    withAnalytics,
    withLogging
)(BaseComponent);

// Or use traditional nesting
const EnhancedComponent = 
    withErrorBoundary(
        withAuth(
            withAnalytics(
                withLogging(BaseComponent)
            )
        )
    );
            

Real-World HOC Examples

1. Authentication HOC


function withAuthentication(WrappedComponent) {
    return function WithAuthenticationComponent(props) {
        const { user, loading } = useAuth(); // Custom hook
        
        if (loading) {
            return <LoadingSpinner />;
        }
        
        if (!user) {
            return <Navigate to="/login" />;
        }
        
        if (props.requiredRole && user.role !== props.requiredRole) {
            return <AccessDenied />;
        }
        
        return <WrappedComponent {...props} user={user} />;
    };
}

// Protected component
function AdminDashboard({ user }) {
    return (
        <div>
            <h1>Admin Dashboard</h1>
            <p>Welcome, {user.name}!</p>
            {/* Admin features */}
        </div>
    );
}

// Secure the component
const SecureAdminDashboard = withAuthentication(AdminDashboard);

// Usage
<SecureAdminDashboard requiredRole="admin" />
            

2. Form Handling HOC


function withFormHandling(initialValues) {
    return function(WrappedComponent) {
        return function WithFormHandlingComponent(props) {
            const [values, setValues] = useState(initialValues);
            const [errors, setErrors] = useState({});
            const [isSubmitting, setIsSubmitting] = useState(false);
            
            const handleChange = (e) => {
                const { name, value } = e.target;
                setValues(prev => ({
                    ...prev,
                    [name]: value
                }));
            };
            
            const handleSubmit = async (e) => {
                e.preventDefault();
                setIsSubmitting(true);
                
                try {
                    await props.onSubmit(values);
                } catch (error) {
                    setErrors(error.validationErrors || {});
                } finally {
                    setIsSubmitting(false);
                }
            };
            
            return (
                <WrappedComponent
                    {...props}
                    values={values}
                    errors={errors}
                    isSubmitting={isSubmitting}
                    handleChange={handleChange}
                    handleSubmit={handleSubmit}
                />
            );
        };
    };
}

// Form component
function LoginForm({ 
    values, 
    errors, 
    isSubmitting, 
    handleChange, 
    handleSubmit 
}) {
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <input
                    name="email"
                    value={values.email}
                    onChange={handleChange}
                    placeholder="Email"
                />
                {errors.email && <span>{errors.email}</span>}
            </div>
            
            <div>
                <input
                    name="password"
                    type="password"
                    value={values.password}
                    onChange={handleChange}
                    placeholder="Password"
                />
                {errors.password && <span>{errors.password}</span>}
            </div>
            
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'Logging in...' : 'Login'}
            </button>
        </form>
    );
}

// Enhanced form
const EnhancedLoginForm = withFormHandling({
    email: '',
    password: ''
})(LoginForm);
            

3. Performance Monitoring HOC


function withPerformanceMonitoring(WrappedComponent) {
    return function WithPerformanceComponent(props) {
        const renderStartTime = useRef(performance.now());
        
        useEffect(() => {
            const renderEndTime = performance.now();
            const renderTime = renderEndTime - renderStartTime.current;
            
            console.log(
                `${WrappedComponent.name} render time: ${renderTime}ms`
            );
            
            // Send to analytics
            analytics.trackPerformance({
                component: WrappedComponent.name,
                renderTime,
                props: Object.keys(props)
            });
        });
        
        return <WrappedComponent {...props} />;
    };
}

// Monitor component performance
const MonitoredDataGrid = withPerformanceMonitoring(DataGrid);
            

HOC Best Practices

graph TD A[HOC Best Practices] --> B[Pass Props Through] A --> C[Copy Static Methods] A --> D[Maintain Display Name] A --> E[Avoid Mutations] A --> F[Maximize Composability] B --> G[Spread props] C --> H[hoistNonReactStatics] D --> I[Debugging] E --> J[Immutable patterns] F --> K[Single responsibility] style A fill:#f9f,stroke:#333,stroke-width:2px

1. Always Pass Props Through


// ❌ Bad - losing props
function withBadHOC(WrappedComponent) {
    return function(props) {
        // Not passing all props
        return <WrappedComponent someProp={props.someProp} />;
    };
}

// ✅ Good - preserving props
function withGoodHOC(WrappedComponent) {
    return function(props) {
        const enhancedProp = 'enhanced';
        return (
            <WrappedComponent 
                {...props} 
                enhanced={enhancedProp} 
            />
        );
    };
}
            

2. Copy Static Methods


import hoistNonReactStatics from 'hoist-non-react-statics';

function withStaticCopy(WrappedComponent) {
    function WithStaticComponent(props) {
        // HOC logic
        return <WrappedComponent {...props} />;
    }
    
    // Copy non-React static methods
    hoistNonReactStatics(WithStaticComponent, WrappedComponent);
    
    return WithStaticComponent;
}

// Original component with static method
class MyComponent extends React.Component {
    static fetchData() {
        // Fetch data
    }
    
    render() {
        return <div>My Component</div>;
    }
}

// Enhanced component preserves static methods
const Enhanced = withStaticCopy(MyComponent);
Enhanced.fetchData(); // Works!
            

3. Set Display Name


function withDisplayName(WrappedComponent) {
    function WithDisplayNameComponent(props) {
        return <WrappedComponent {...props} />;
    }
    
    // Set display name for debugging
    const wrappedName = WrappedComponent.displayName 
        || WrappedComponent.name 
        || 'Component';
    
    WithDisplayNameComponent.displayName = 
        `withDisplayName(${wrappedName})`;
    
    return WithDisplayNameComponent;
}
            

HOC vs Other Patterns

graph TD A[Component Enhancement Patterns] --> B[HOCs] A --> C[Render Props] A --> D[Custom Hooks] B --> E[Wrapper Components] B --> F[Props Injection] C --> G[Dynamic Rendering] C --> H[Function as Children] D --> I[Reusable Logic] D --> J[State Management] style A fill:#f9f,stroke:#333,stroke-width:2px

When to Use HOCs vs Hooks


// HOC approach
function withWindowSize(WrappedComponent) {
    return function WithWindowSizeComponent(props) {
        const [size, setSize] = useState({
            width: window.innerWidth,
            height: window.innerHeight
        });
        
        useEffect(() => {
            const handleResize = () => {
                setSize({
                    width: window.innerWidth,
                    height: window.innerHeight
                });
            };
            
            window.addEventListener('resize', handleResize);
            return () => window.removeEventListener('resize', handleResize);
        }, []);
        
        return <WrappedComponent {...props} windowSize={size} />;
    };
}

// Hook approach (modern, preferred)
function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });
    
    useEffect(() => {
        const handleResize = () => {
            setSize({
                width: window.innerWidth,
                height: window.innerHeight
            });
        };
        
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
    }, []);
    
    return size;
}

// Using the hook
function ComponentWithHook() {
    const windowSize = useWindowSize();
    
    return <div>Width: {windowSize.width}</div>;
}
            

Common HOC Pitfalls

1. HOCs Inside Render


// ❌ Bad - creates new component on every render
function ParentComponent() {
    return (
        <div>
            {React.createElement(withLoading(MyComponent), props)}
        </div>
    );
}

// ✅ Good - create enhanced component once
const MyComponentWithLoading = withLoading(MyComponent);

function ParentComponent() {
    return (
        <div>
            <MyComponentWithLoading {...props} />
        </div>
    );
}
            

2. Mutating the Original Component


// ❌ Bad - mutating the original component
function withMutation(WrappedComponent) {
    WrappedComponent.prototype.newMethod = function() {
        // Don't do this!
    };
    
    return WrappedComponent;
}

// ✅ Good - create a new component
function withEnhancement(WrappedComponent) {
    return class extends React.Component {
        newMethod() {
            // Safe method
        }
        
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}
            

Modern Alternatives to HOCs

While HOCs are still useful, many use cases are now better served by:


// Modern alternative using hooks
function useAuth() {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    
    // Auth logic here
    
    return { user, loading };
}

function SecureComponent() {
    const { user, loading } = useAuth();
    
    if (loading) return <LoadingSpinner />;
    if (!user) return <Navigate to="/login" />;
    
    return <ProtectedContent user={user} />;
}
            

Practice Exercises

Exercise 1: Create a Loading HOC

Build a HOC that shows a loading spinner:


function withLoading(WrappedComponent) {
    // Create a HOC that:
    // - Shows loading spinner when isLoading prop is true
    // - Passes through all other props
    // - Handles error state
}
            

Exercise 2: Build an Analytics HOC

Create a HOC that tracks component usage:


function withAnalytics(WrappedComponent) {
    // Create a HOC that:
    // - Tracks component mount/unmount
    // - Logs prop changes
    // - Records user interactions
}
            

Exercise 3: Compose Multiple HOCs

Create a system for composing multiple HOCs:


function createEnhancedComponent() {
    // Create a component with:
    // - Authentication
    // - Loading state
    // - Error boundary
    // - Performance monitoring
    // Use HOC composition
}
            

Key Takeaways

What's Next?

Congratulations! You've completed Week 4 of the React module. Next week, we'll dive into advanced React concepts including hooks in depth, React Router, and performance optimization. Get ready to take your React skills to the next level!

Homework