Higher-Order Components (HOCs) in React

What are Higher-Order Components?

A Higher-Order Component (HOC) is an advanced pattern in React that involves a function that takes a component and returns a new enhanced component. HOCs allow you to reuse component logic, add additional props, and modify the behavior of components without modifying their implementation.

// Basic HOC pattern
function withExtraProps(WrappedComponent) {
  // Return a new enhanced component
  return function EnhancedComponent(props) {
    // Add extra props
    const extraProps = { extraData: 'Some data' };
    
    // Return the wrapped component with combined props
    return <WrappedComponent {...props} {...extraProps} />;
  };
}

// Usage
const EnhancedComponent = withExtraProps(MyComponent);

How HOCs Work

HOCs follow a simple principle: they take a component as input and return a new component with enhanced capabilities. They don’t modify the original component but compose it with new functionality.

// HOC that adds logging
function withLogging(WrappedComponent) {
  return function WithLogging(props) {
    console.log(`Component ${WrappedComponent.name} rendering with props:`, props);
    
    return <WrappedComponent {...props} />;
  };
}

// Original component
function UserProfile({ name, email }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  );
}

// Enhanced component with logging
const UserProfileWithLogging = withLogging(UserProfile);

// Usage
<UserProfileWithLogging name="John" email="john@example.com" />

Common Use Cases for HOCs

1. Code Reuse and Cross-Cutting Concerns

HOCs are excellent for sharing code between components:

// HOC for handling loading states
function withLoader(WrappedComponent, loadingMessage = 'Loading...') {
  return function WithLoader({ isLoading, ...props }) {
    if (isLoading) {
      return <div className="loader">{loadingMessage}</div>;
    }
    
    return <WrappedComponent {...props} />;
  };
}

// Usage
const UserListWithLoader = withLoader(UserList);
const ProductListWithLoader = withLoader(ProductList, 'Fetching products...');

2. Props Manipulation

HOCs can add, modify, or filter props:

// HOC that injects a theme prop
function withTheme(WrappedComponent) {
  return function WithTheme(props) {
    // Get theme from context or configuration
    const theme = useContext(ThemeContext);
    
    // Pass theme as a prop
    return <WrappedComponent {...props} theme={theme} />;
  };
}

3. State Abstraction

HOCs can manage state and pass it as props:

// HOC that manages form state
function withFormState(WrappedComponent, initialState = {}) {
  return function WithFormState(props) {
    const [formData, setFormData] = useState(initialState);
    
    const handleChange = (e) => {
      const { name, value } = e.target;
      setFormData(prevData => ({
        ...prevData,
        [name]: value
      }));
    };
    
    const handleSubmit = (e) => {
      e.preventDefault();
      console.log('Form submitted with:', formData);
      // Additional submission logic
    };
    
    return (
      <WrappedComponent
        {...props}
        formData={formData}
        handleChange={handleChange}
        handleSubmit={handleSubmit}
      />
    );
  };
}

// Usage
function LoginForm({ formData, handleChange, handleSubmit }) {
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={formData.username || ''}
        onChange={handleChange}
        placeholder="Username"
      />
      <input
        type="password"
        name="password"
        value={formData.password || ''}
        onChange={handleChange}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

const LoginFormWithState = withFormState(LoginForm, { username: '', password: '' });

4. Conditional Rendering

HOCs can conditionally render components:

// HOC for authentication protection
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const { isAuthenticated, user } = useAuth();
    
    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }
    
    return <WrappedComponent {...props} user={user} />;
  };
}

// Usage
const ProtectedDashboard = withAuth(Dashboard);

HOC Composition

Multiple HOCs can be composed together:

// Compose multiple HOCs
const EnhancedComponent = withAuth(withTheme(withLoader(MyComponent)));

// Using a compose utility for better readability
const enhance = compose(
  withAuth,
  withTheme,
  withLoader
);

const EnhancedComponent = enhance(MyComponent);

HOCs vs. Hooks

With the introduction of Hooks in React 16.8, many use cases for HOCs can now be handled with custom hooks:

// HOC approach
const UserProfileWithData = withUser(UserProfile);

// Hook approach
function UserProfile() {
  const user = useUser();
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

When to Use HOCs vs. Hooks:

  • Use HOCs when:

    • You need to wrap a component with additional JSX
    • You want to reuse the same enhancement across many components
    • You need to work with class components
  • Use Hooks when:

    • You want to reuse stateful logic without changing component hierarchy
    • You want to split one component into smaller functions
    • You want simpler, more readable code

Best Practices for HOCs

1. Use Descriptive Names

Name your HOCs and wrapped components clearly:

// Good
const withLoader = (WrappedComponent) => {
  function WithLoader(props) { /* ... */ }
  return WithLoader;
};

// Better: Add displayName for debugging
const withLoader = (WrappedComponent) => {
  function WithLoader(props) { /* ... */ }
  WithLoader.displayName = `WithLoader(${getDisplayName(WrappedComponent)})`;
  return WithLoader;
};

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

2. Don’t Mutate the Original Component

Create a new component instead of modifying the input:

// Bad
function withStyles(WrappedComponent) {
  WrappedComponent.prototype.componentDidMount = function() {
    // Mutation!
  };
  return WrappedComponent;
}

// Good
function withStyles(WrappedComponent) {
  return function WithStyles(props) {
    // No mutation, just composition
    return <WrappedComponent {...props} className="styled" />;
  };
}

3. Pass Unrelated Props Through

Forward props that aren’t used by the HOC:

function withTheme(WrappedComponent) {
  return function WithTheme({ specificThemeProp, ...restProps }) {
    // Use specificThemeProp for HOC logic
    const theme = determineTheme(specificThemeProp);
    
    // Pass all other props through
    return <WrappedComponent {...restProps} theme={theme} />;
  };
}

4. Maximize Composability

Design HOCs to be easily composable with other HOCs:

// Each HOC does one thing well
const withData = (WrappedComponent) => { /* ... */ };
const withLoading = (WrappedComponent) => { /* ... */ };
const withErrorHandling = (WrappedComponent) => { /* ... */ };

// Compose them together
const enhance = compose(
  withData,
  withLoading,
  withErrorHandling
);

const EnhancedComponent = enhance(MyComponent);

Interview Tips

  • Explain that HOCs are a pattern for reusing component logic, not a React API feature
  • Discuss the difference between HOCs, render props, and hooks for code reuse
  • Mention common libraries that use HOCs (Redux’s connect, React Router’s withRouter)
  • Be prepared to discuss HOC limitations (wrapper hell, naming collisions, static methods)
  • Explain how HOCs fit into modern React development with hooks
  • Demonstrate understanding of when HOCs are still valuable despite hooks

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.