Chapter 9:
Higher-Order Components (Legacy Pattern)

Objectives

In this chapter, readers will:

  • Understand higher-order components (HOCs) as a legacy React pattern for code reuse
  • Recognize HOCs in existing codebases and understand their purpose
  • Compare HOCs with modern alternatives like custom hooks and render props
  • Migrate from HOC patterns to modern React approaches when appropriate
  • Evaluate the rare cases where HOCs are still justified in 2025

Chapter Outline

Higher-Order Components: Legacy Pattern Overview

⚠️ Legacy Pattern Warning: Higher-Order Components (HOCs) are a pre-2019 React pattern for reusing component logic. While still functional in 2025, they are not recommended for new development. Modern React applications should use custom hooks for logic reuse, which provide better performance, simpler testing, and cleaner code.

Why Learn HOCs in 2025?

  • Understanding legacy codebases
  • Maintaining existing applications
  • Migration planning to modern patterns
  • Third-party library comprehension

What Are Higher-Order Components?

A Higher-Order Component is a function that takes a component and returns a new component with additional functionality. This pattern was inspired by higher-order functions in JavaScript.

Basic HOC Structure:

// HOC signature: (Component) => EnhancedComponent
function withSomeFeature(WrappedComponent) {
  return function EnhancedComponent(props) {
    // Add functionality here
    return <WrappedComponent {...props} additionalProp="value" />;
  };
}

Understanding HOCs for Legacy Codebases

Classic HOC Example: withHover (Legacy)

Here’s how HOCs were commonly implemented in pre-2019 React applications:

Legacy Class-Based HOC:

import React from 'react';

function withHover(Component) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hovering: false };
    }

    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });

    render() {
      return (
        <Component
          {...this.props}
          hovering={this.state.hovering}
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        />
      );
    }
  };
}

export default withHover;

Using the Legacy HOC:

import React from 'react';
import withHover from './withHover';

// Base component
function Button({ hovering, children, ...props }) {
  return (
    <button
      style={{
        backgroundColor: hovering ? '#0066cc' : '#0080ff',
        color: 'white',
        padding: '10px 20px',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
        transition: 'background-color 0.2s'
      }}
      {...props}
    >
      {children}
    </button>
  );
}

// Enhanced component using HOC
const HoverableButton = withHover(Button);

// Usage
function App() {
  return (
    <div>
      <HoverableButton>Hover over me!</HoverableButton>
    </div>
  );
}

Modern Function-Based HOC (Still Legacy)

Even with function components, HOCs remained complex:

import React, { useState } from 'react';

function withHover(WrappedComponent) {
  return function HoverableComponent(props) {
    const [hovering, setHovering] = useState(false);

    const handleMouseOver = () => setHovering(true);
    const handleMouseOut = () => setHovering(false);

    return (
      <WrappedComponent
        {...props}
        hovering={hovering}
        onMouseOver={handleMouseOver}
        onMouseOut={handleMouseOut}
      />
    );
  };
}

Problems with HOCs

1. Wrapper Hell:

// Multiple HOCs create deeply nested components
export default withAuth(
  withLoading(
    withError(
      withHover(
        withTheme(MyComponent)
      )
    )
  )
);

2. Prop Collisions:

// Props from different HOCs can conflict
const EnhancedComponent = withUser(withAuth(Component));
// What if both HOCs pass a 'loading' prop?

3. Static Method Loss:

class MyComponent extends React.Component {
  static displayName = 'MyComponent';
  static someMethod() { return 'test'; }
}

const Enhanced = withHOC(MyComponent);
// Enhanced.someMethod is undefined!
// Enhanced.displayName is 'withHOC(MyComponent)'

4. Difficult Testing:

// Testing the wrapped component is complex
const Enhanced = withHover(withAuth(MyComponent));
// How do you test MyComponent in isolation?

Modern Alternatives to HOCs

2025 Recommendation: Use custom hooks instead of HOCs for logic reuse.

Custom Hooks: The Modern Approach

Modern useHover Hook:

import { useState, useRef, useEffect } from 'react';

function useHover() {
  const [hovering, setHovering] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const handleMouseOver = () => setHovering(true);
    const handleMouseOut = () => setHovering(false);

    element.addEventListener('mouseenter', handleMouseOver);
    element.addEventListener('mouseleave', handleMouseOut);

    return () => {
      element.removeEventListener('mouseenter', handleMouseOver);
      element.removeEventListener('mouseleave', handleMouseOut);
    };
  }, []);

  return { hovering, ref };
}

Using the Modern Hook:

import React from 'react';
import { useHover } from './useHover';

function Button({ children, ...props }) {
  const { hovering, ref } = useHover();

  return (
    <button
      ref={ref}
      style={{
        backgroundColor: hovering ? '#0066cc' : '#0080ff',
        color: 'white',
        padding: '10px 20px',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
        transition: 'background-color 0.2s'
      }}
      {...props}
    >
      {children}
    </button>
  );
}

function App() {
  return (
    <div>
      <Button>Hover over me!</Button>
    </div>
  );
}

Benefits of Custom Hooks over HOCs

1. No Wrapper Components:

// HOC: Creates wrapper component
const Enhanced = withHover(Component); // Extra component in tree

// Hook: No wrapper needed
function Component() {
  const { hovering, ref } = useHover(); // Direct usage
  return <div ref={ref}>Content</div>;
}

2. Clearer Data Flow:

// HOC: Implicit prop injection
function Component({ hovering }) { // Where did 'hovering' come from?
  return <div>{hovering ? 'Hovered' : 'Not hovered'}</div>;
}

// Hook: Explicit data flow
function Component() {
  const { hovering } = useHover(); // Clear source
  return <div>{hovering ? 'Hovered' : 'Not hovered'}</div>;
}

3. Better TypeScript Support:

// HOC: Complex typing
interface WithHoverProps {
  hovering: boolean;
  onMouseOver: () => void;
  onMouseOut: () => void;
}

function withHover<P extends object>(
  Component: React.ComponentType<P & WithHoverProps>
): React.ComponentType<Omit<P, keyof WithHoverProps>> {
  // Complex implementation
}

// Hook: Simple typing
function useHover(): {
  hovering: boolean;
  ref: React.RefObject<HTMLElement>;
} {
  // Simple implementation
}

4. Easier Testing:

// Hook: Test in isolation
import { renderHook, act } from '@testing-library/react';
import { useHover } from './useHover';

test('useHover should toggle on mouse events', () => {
  const { result } = renderHook(() => useHover());
  expect(result.current.hovering).toBe(false);
  
  act(() => {
    // Simulate mouse enter
  });
  expect(result.current.hovering).toBe(true);
});

Migration Strategies

HOC to Custom Hook Migration

Step 1: Identify the Logic

// Original HOC
function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      fetchUser().then(setUser).finally(() => setLoading(false));
    }, []);

    if (loading) return <div>Loading...</div>;
    if (!user) return <div>Please log in</div>;

    return <Component {...props} user={user} />;
  };
}

Step 2: Extract to Custom Hook

// New custom hook
function useAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUser()
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  return { user, loading, error };
}

Step 3: Update Components

// Before: Using HOC
const MyComponent = withAuth(function MyComponent({ user }) {
  return <div>Welcome, {user.name}!</div>;
});

// After: Using custom hook
function MyComponent() {
  const { user, loading, error } = useAuth();

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>Please log in</div>;

  return <div>Welcome, {user.name}!</div>;
}

Gradual Migration Approach

1. Create Hook First:

// Add new hook alongside existing HOC
function useFeature() {
  // Modern implementation
}

function withFeature(Component) {
  // Keep existing HOC for backward compatibility
  return function WrappedComponent(props) {
    const featureProps = useFeature();
    return <Component {...props} {...featureProps} />;
  };
}

2. Update Components One by One:

// Migrate components individually
function ComponentA() {
  const feature = useFeature(); // New approach
  return <div>{/* ... */}</div>;
}

const ComponentB = withFeature(ComponentBBase); // Keep old approach temporarily

3. Remove HOCs When All Components Migrated:

// Finally remove the HOC
// function withFeature() { ... } // Delete this

When HOCs Are Still Appropriate

⚠️ Rare Cases Only: These scenarios are uncommon in modern React applications.

1. Third-Party Library Integration

When integrating with libraries that expect specific component structures:

// Some legacy libraries might require HOC pattern
function withLegacyLibrary(Component) {
  return React.forwardRef((props, ref) => {
    useEffect(() => {
      // Initialize legacy library
      legacyLib.init(ref.current);
      return () => legacyLib.cleanup();
    }, []);

    return <Component ref={ref} {...props} />;
  });
}

2. Framework Migration

During migration from other frameworks:

// Temporary HOC during migration from class-based system
function withClassCompat(Component) {
  return class extends React.Component {
    // Temporary compatibility layer
    render() {
      return <Component {...this.props} />;
    }
  };
}

3. Conditional Component Enhancement

Very specific cases where conditional wrapping is needed:

function withConditionalFeature(Component, condition) {
  if (!condition) {
    return Component; // Return original component unchanged
  }
  
  return function EnhancedComponent(props) {
    return (
      <div className="enhanced-wrapper">
        <Component {...props} />
      </div>
    );
  };
}

Modern HOC Best Practices (If You Must Use Them)

1. Use Function Components with Hooks

// Modern HOC structure
function withFeature(WrappedComponent) {
  const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
  
  function WithFeatureComponent(props) {
    const featureData = useFeatureHook();
    
    return <WrappedComponent {...props} {...featureData} />;
  }
  
  WithFeatureComponent.displayName = `withFeature(${displayName})`;
  WithFeatureComponent.WrappedComponent = WrappedComponent;
  
  return React.memo(WithFeatureComponent);
}

2. Preserve Static Methods and Properties

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

function withFeature(WrappedComponent) {
  function WithFeatureComponent(props) {
    return <WrappedComponent {...props} />;
  }
  
  // Copy static methods
  hoistNonReactStatics(WithFeatureComponent, WrappedComponent);
  
  return WithFeatureComponent;
}

3. Use React.forwardRef for Ref Forwarding

function withFeature(Component) {
  const WithFeatureComponent = React.forwardRef((props, ref) => {
    const featureProps = useFeature();
    return <Component ref={ref} {...props} {...featureProps} />;
  });
  
  WithFeatureComponent.displayName = `withFeature(${Component.displayName || Component.name})`;
  
  return WithFeatureComponent;
}

Summary and Recommendations

2025 React Development Guidelines

✅ Recommended Approach:

  1. Use Custom Hooks for logic reuse
  2. Use Render Props for complex sharing scenarios
  3. Use Composition over inheritance
  4. Use Context API for deep prop passing

❌ Avoid These Patterns:

  1. Higher-Order Components (except rare cases)
  2. Mixins (deprecated and removed)
  3. Class components (use function components)

Migration Priority

High Priority:

  • HOCs that handle authentication
  • HOCs that manage loading states
  • HOCs that provide theme/styling

Medium Priority:

  • HOCs that add event listeners
  • HOCs that format data
  • HOCs that handle forms

Low Priority:

  • HOCs that only pass props through
  • HOCs that add static content
  • HOCs that wrap for styling only

Modern Alternatives Summary

Legacy Pattern Modern Alternative Benefits
withAuth(Component) useAuth() hook Direct data access, better testing
withLoading(Component) useLoading() hook No wrapper components, clearer logic
withHover(Component) useHover() hook Better performance, simpler API
withTheme(Component) useTheme() hook or Context Type safety, easier debugging

Key Takeaways

  1. HOCs are legacy: Don’t use them in new code
  2. Custom hooks are superior: Better performance, testing, and developer experience
  3. Understand for maintenance: You’ll encounter HOCs in existing codebases
  4. Migrate gradually: Don’t rush migration, but plan for it
  5. Consider the context: Some third-party libraries might still use HOCs

Final Recommendation for 2025: Learn HOCs to understand legacy code, but always choose custom hooks for new development. The React ecosystem has evolved beyond HOCs, and modern patterns provide better solutions for component logic reuse.