What are Higher-Order Components (HOCs)?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React

Quick Answer

A Higher-Order Component (HOC) — is a function that takes a component and returns a new component with enhanced functionality. It’s a composition pattern for reusing component logic.

Basic HOC example:

// HOC for adding logging
const withLogging = (WrappedComponent) => {
  return function WithLoggingComponent(props) {
    useEffect(() => {
      console.log('Component mounted:', WrappedComponent.name);
    }, []);
    
    return <WrappedComponent {...props} />;
  };
};
 
// Usage
const Button = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
);
 
const ButtonWithLogging = withLogging(Button);

Detailed Answer

HOCs — are an advanced pattern in React for reusing component logic. They are not part of the React API, but emerge from React’s compositional nature.

HOC Anatomy

Basic Structure

// HOC takes a component
const withEnhancement = (WrappedComponent) => {
  // Returns a new component
  const EnhancedComponent = (props) => {
    // Adds new logic
    const [state, setState] = useState();
    
    // Renders the original component with new props
    return (
      <WrappedComponent 
        {...props} 
        enhancedProp={state}
      />
    );
  };
  
  // Set displayName for debugging
  EnhancedComponent.displayName = 
    `withEnhancement(${WrappedComponent.displayName || WrappedComponent.name})`;
  
  return EnhancedComponent;
};

Common HOC Examples

1. Authentication HOC

const withAuth = (WrappedComponent) => {
  return function WithAuthComponent(props) {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [isLoading, setIsLoading] = useState(true);
    
    useEffect(() => {
      // Check authentication
      checkAuth()
        .then(authenticated => {
          setIsAuthenticated(authenticated);
          setIsLoading(false);
        });
    }, []);
    
    if (isLoading) {
      return <div>Loading...</div>;
    }
    
    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }
    
    return <WrappedComponent {...props} />;
  };
};
 
// Usage
const Dashboard = () => <div>Dashboard</div>;
const ProtectedDashboard = withAuth(Dashboard);

2. Data Fetching HOC

const withDataFetching = (url) => (WrappedComponent) => {
  return function WithDataFetchingComponent(props) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
      fetch(url)
        .then(res => res.json())
        .then(data => {
          setData(data);
          setLoading(false);
        })
        .catch(err => {
          setError(err);
          setLoading(false);
        });
    }, []);
    
    return (
      <WrappedComponent 
        {...props}
        data={data}
        loading={loading}
        error={error}
      />
    );
  };
};
 
// Usage
const UserList = ({ data, loading, error }) => {
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};
 
const UsersWithData = withDataFetching('/api/users')(UserList);

3. Window Size Tracking HOC

const withWindowSize = (WrappedComponent) => {
  return function WithWindowSizeComponent(props) {
    const [windowSize, setWindowSize] = useState({
      width: window.innerWidth,
      height: window.innerHeight
    });
    
    useEffect(() => {
      const handleResize = () => {
        setWindowSize({
          width: window.innerWidth,
          height: window.innerHeight
        });
      };
      
      window.addEventListener('resize', handleResize);
      
      return () => {
        window.removeEventListener('resize', handleResize);
      };
    }, []);
    
    return (
      <WrappedComponent 
        {...props}
        windowSize={windowSize}
      />
    );
  };
};
 
// Usage
const ResponsiveComponent = ({ windowSize }) => (
  <div>
    Width: {windowSize.width}px
    {windowSize.width < 768 ? ' (Mobile)' : ' (Desktop)'}
  </div>
);
 
const ResponsiveWithSize = withWindowSize(ResponsiveComponent);

HOC Composition

Sequential Application

// Multiple HOCs in sequence
const enhance = compose(
  withAuth,
  withWindowSize,
  withLogging
);
 
const EnhancedComponent = enhance(BaseComponent);
 
// Or manually
const EnhancedComponent = 
  withAuth(
    withWindowSize(
      withLogging(BaseComponent)
    )
  );

Compose Implementation

const compose = (...hocs) => (Component) =>
  hocs.reduceRight((acc, hoc) => hoc(acc), Component);
 
// Usage
const enhance = compose(
  withRouter,
  withAuth,
  connect(mapStateToProps)
);
 
const EnhancedApp = enhance(App);

Advanced Patterns

1. HOC with Configuration

const withTheme = (theme = 'light') => (WrappedComponent) => {
  return function WithThemeComponent(props) {
    const themeConfig = {
      light: {
        background: '#fff',
        color: '#000'
      },
      dark: {
        background: '#000',
        color: '#fff'
      }
    };
    
    return (
      <div style={themeConfig[theme]}>
        <WrappedComponent {...props} theme={theme} />
      </div>
    );
  };
};
 
// Usage
const ThemedButton = withTheme('dark')(Button);

2. HOC with Ref Forwarding

const withRefForwarding = (WrappedComponent) => {
  const WithRef = React.forwardRef((props, ref) => {
    return <WrappedComponent {...props} forwardedRef={ref} />;
  });
  
  WithRef.displayName = 
    `withRefForwarding(${WrappedComponent.displayName || WrappedComponent.name})`;
  
  return WithRef;
};
 
// Component using ref
const FancyInput = ({ forwardedRef, ...props }) => (
  <input ref={forwardedRef} {...props} />
);
 
const ForwardedInput = withRefForwarding(FancyInput);
 
// Usage
const App = () => {
  const inputRef = useRef();
  
  return <ForwardedInput ref={inputRef} />;
};

Best Practices

1. Don’t Mutate the Original Component

// ❌ Bad: mutating the component
const withBadEnhancement = (WrappedComponent) => {
  WrappedComponent.prototype.componentDidUpdate = function() {
    console.log('Updated!');
  };
  return WrappedComponent;
};
 
// ✅ Good: composition
const withGoodEnhancement = (WrappedComponent) => {
  return class extends React.Component {
    componentDidUpdate() {
      console.log('Updated!');
    }
    
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

2. Pass Through All Props

// ❌ Bad: losing props
const withBadHOC = (WrappedComponent) => {
  return function BadHOC({ someProp }) {
    return <WrappedComponent someProp={someProp} />;
  };
};
 
// ✅ Good: passing all props
const withGoodHOC = (WrappedComponent) => {
  return function GoodHOC(props) {
    return <WrappedComponent {...props} />;
  };
};

3. Maximize Composability

// ❌ Bad: HOC does too much
const withEverything = (WrappedComponent) => {
  return function WithEverything(props) {
    // Authentication
    // Data loading
    // Theme
    // Logging
    // ...too much logic
  };
};
 
// ✅ Good: separate HOCs for each concern
const enhance = compose(
  withAuth,
  withDataFetching('/api/data'),
  withTheme('dark'),
  withLogging
);

HOC vs Other Patterns

HOC vs Render Props

// HOC
const withMouse = (Component) => (props) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  // ... logic
  return <Component {...props} mouse={position} />;
};
 
// Render Props
const Mouse = ({ render }) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  // ... logic
  return render(position);
};
 
// Using Render Props
<Mouse render={position => <Component mouse={position} />} />

HOC vs Custom Hooks

// HOC
const withWindowSize = (Component) => (props) => {
  const [size, setSize] = useState(getWindowSize());
  // ... logic
  return <Component {...props} windowSize={size} />;
};
 
// Custom Hook
const useWindowSize = () => {
  const [size, setSize] = useState(getWindowSize());
  // ... logic
  return size;
};
 
// Using Hook
const Component = () => {
  const windowSize = useWindowSize();
  // ... use windowSize
};

When to Use HOCs

Good for:

  • Reusing logic between many components
  • Adding functionality without changing the component
  • Conditional rendering based on external factors
  • Injecting props from external sources

Not good when:

  • More flexible composition is needed (use hooks)
  • Logic is specific to one component
  • Access to component’s internal state is required

Debugging HOCs

// Setting displayName for DevTools
const withDebugInfo = (WrappedComponent) => {
  const WithDebugInfo = (props) => {
    useEffect(() => {
      console.log(`${WrappedComponent.name} rendered with props:`, props);
    });
    
    return <WrappedComponent {...props} />;
  };
  
  // Important for React DevTools
  WithDebugInfo.displayName = 
    `withDebugInfo(${WrappedComponent.displayName || WrappedComponent.name})`;
  
  return WithDebugInfo;
};

HOCs remain a powerful tool for creating reusable logic, although in modern React, hooks are often preferred for solving the same problems.


Want more interview prep articles? Subscribe to EasyAdvice, bookmark the site, and improve every day 💪