Higher-Order Components (HOC)

3 interview angles 8 min read source

Higher-Order Components (HOC)

Higher-Order Components (HOC) are advanced React patterns that allow you to reuse component logic by wrapping components with additional functionality.

Table of Contents


What is a HOC?

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

Key Concepts:

  • Function that returns a component: HOC is a function, not a component
  • Composition over inheritance: Reuse logic through composition
  • Props manipulation: Can add, modify, or filter props
  • Cross-cutting concerns: Handle common functionality across components

Basic HOC Structure:

// HOC is a function that takes a component and returns a new component
function withEnhancement(WrappedComponent) {
  // Return a new component
  return function EnhancedComponent(props) {
    // Add logic here
    const enhancedProps = {
      ...props,
      enhanced: true
    };

    // Return the wrapped component with enhanced props
    return <WrappedComponent {...enhancedProps} />;
  };
}

// Usage
const EnhancedButton = withEnhancement(Button);

HOC Pattern

1. Basic HOC:

// Simple HOC that adds a prop
function withGreeting(WrappedComponent) {
  return function EnhancedComponent(props) {
    return (
      <WrappedComponent
        {...props}
        greeting="Hello from HOC!"
      />
    );
  };
}

// Original component
function Button({ onClick, children, greeting }) {
  return (
    <button onClick={onClick}>
      {greeting} - {children}
    </button>
  );
}

// Enhanced component
const EnhancedButton = withGreeting(Button);

// Usage
function App() {
  return (
    <EnhancedButton onClick={() => alert('Clicked!')}>
      Click me
    </EnhancedButton>
  );
}

2. HOC with State:

// HOC that manages state
function withCounter(WrappedComponent) {
  return class EnhancedComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = { count: 0 };
    }

    increment = () => {
      this.setState(prevState => ({
        count: prevState.count + 1
      }));
    };

    render() {
      return (
        <WrappedComponent
          {...this.props}
          count={this.state.count}
          increment={this.increment}
        />
      );
    }
  };
}

// Component that uses the counter
function DisplayCounter({ count, increment, label }) {
  return (
    <div>
      <h3>{label}</h3>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

// Enhanced component
const CounterDisplay = withCounter(DisplayCounter);

// Usage
function App() {
  return (
    <div>
      <CounterDisplay label="First Counter" />
      <CounterDisplay label="Second Counter" />
    </div>
  );
}

3. HOC with Props Transformation:

// HOC that transforms props
function withUpperCase(WrappedComponent) {
  return function EnhancedComponent(props) {
    // Transform text props to uppercase
    const transformedProps = Object.keys(props).reduce((acc, key) => {
      if (typeof props[key] === 'string') {
        acc[key] = props[key].toUpperCase();
      } else {
        acc[key] = props[key];
      }
      return acc;
    }, {});

    return <WrappedComponent {...transformedProps} />;
  };
}

// Original component
function TextDisplay({ title, description }) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{description}</p>
    </div>
  );
}

// Enhanced component
const UpperCaseText = withUpperCase(TextDisplay);

// Usage
function App() {
  return (
    <UpperCaseText
      title="hello world"
      description="this will be uppercase"
    />
  );
  // Renders: "HELLO WORLD" and "THIS WILL BE UPPERCASE"
}

Common HOC Examples

1. withLoading HOC:

// HOC that handles loading state
function withLoading(WrappedComponent) {
  return function EnhancedComponent({ loading, ...props }) {
    if (loading) {
      return <div>Loading...</div>;
    }

    return <WrappedComponent {...props} />;
  };
}

// Component that needs loading state
function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

// Enhanced component
const UserProfileWithLoading = withLoading(UserProfile);

// Usage
function App() {
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState(null);

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

  return (
    <UserProfileWithLoading
      loading={loading}
      user={user}
    />
  );
}

2. withErrorBoundary HOC:

// HOC that provides error boundary functionality
function withErrorBoundary(WrappedComponent) {
  return class EnhancedComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error) {
      return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
      console.error('Error caught by HOC:', error, errorInfo);
    }

    render() {
      if (this.state.hasError) {
        return (
          <div>
            <h2>Something went wrong.</h2>
            <button onClick={() => this.setState({ hasError: false })}>
              Try again
            </button>
          </div>
        );
      }

      return <WrappedComponent {...this.props} />;
    }
  };
}

// Component that might throw errors
function RiskyComponent({ data }) {
  if (!data) {
    throw new Error('Data is required');
  }

  return <div>{data}</div>;
}

// Enhanced component
const SafeComponent = withErrorBoundary(RiskyComponent);

// Usage
function App() {
  return (
    <SafeComponent data={null} /> // Will show error boundary
  );
}

3. withAuthentication HOC:

// HOC that handles authentication
function withAuthentication(WrappedComponent) {
  return function EnhancedComponent(props) {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [user, setUser] = useState(null);

    useEffect(() => {
      // Check authentication status
      checkAuth().then(authData => {
        setIsAuthenticated(authData.isAuthenticated);
        setUser(authData.user);
      });
    }, []);

    if (!isAuthenticated) {
      return <LoginForm />;
    }

    return (
      <WrappedComponent
        {...props}
        user={user}
        isAuthenticated={isAuthenticated}
      />
    );
  };
}

// Component that needs authentication
function Dashboard({ user, isAuthenticated }) {
  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <p>You are authenticated: {isAuthenticated ? 'Yes' : 'No'}</p>
    </div>
  );
}

// Enhanced component
const AuthenticatedDashboard = withAuthentication(Dashboard);

// Usage
function App() {
  return <AuthenticatedDashboard />;
}

Advanced HOC Patterns

1. HOC with Display Name:

// HOC that sets proper display name for debugging
function withDisplayName(WrappedComponent, displayName) {
  const EnhancedComponent = function(props) {
    return <WrappedComponent {...props} />;
  };

  // Set display name for debugging
  EnhancedComponent.displayName = `${displayName}(${getDisplayName(WrappedComponent)})`;

  return EnhancedComponent;
}

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

// Usage
const EnhancedButton = withDisplayName(Button, 'withGreeting');
// Display name will be: "withGreeting(Button)"

2. HOC with Ref Forwarding:

// HOC that forwards refs
function withRef(WrappedComponent) {
  return React.forwardRef((props, ref) => {
    return <WrappedComponent {...props} ref={ref} />;
  });
}

// Component that accepts ref
const Button = React.forwardRef((props, ref) => {
  return <button ref={ref} {...props} />;
});

// Enhanced component
const EnhancedButton = withRef(Button);

// Usage
function App() {
  const buttonRef = useRef();

  useEffect(() => {
    buttonRef.current.focus();
  }, []);

  return <EnhancedButton ref={buttonRef}>Click me</EnhancedButton>;
}

3. HOC with Multiple Enhancements:

// Compose multiple HOCs
function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

// Multiple HOCs
const withLogging = (WrappedComponent) => {
  return function EnhancedComponent(props) {
    console.log('Component rendered with props:', props);
    return <WrappedComponent {...props} />;
  };
};

const withTiming = (WrappedComponent) => {
  return class EnhancedComponent extends React.Component {
    componentDidMount() {
      console.time('Component render time');
    }

    componentDidUpdate() {
      console.timeEnd('Component render time');
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

// Compose HOCs
const EnhancedComponent = compose(
  withLogging,
  withTiming,
  withCounter
)(Button);

Best Practices

1. Don’t Mutate the Wrapped Component:

// Good: Don't mutate the original component
function withEnhancement(WrappedComponent) {
  return function EnhancedComponent(props) {
    return <WrappedComponent {...props} />;
  };
}

// Bad: Don't mutate the original component
function withEnhancement(WrappedComponent) {
  WrappedComponent.newProp = 'value'; // Don't do this
  return WrappedComponent;
}

2. Pass Through Unrelated Props:

// Good: Pass through all props
function withEnhancement(WrappedComponent) {
  return function EnhancedComponent(props) {
    const { enhanced, ...restProps } = props;
    return <WrappedComponent {...restProps} enhanced={enhanced} />;
  };
}

// Bad: Don't filter out props unnecessarily
function withEnhancement(WrappedComponent) {
  return function EnhancedComponent({ enhanced, ...props }) {
    return <WrappedComponent enhanced={enhanced} />; // Lost other props
  };
}

3. Use Display Names for Debugging:

// Good: Set display name
function withEnhancement(WrappedComponent) {
  const EnhancedComponent = function(props) {
    return <WrappedComponent {...props} />;
  };

  EnhancedComponent.displayName = `withEnhancement(${getDisplayName(WrappedComponent)})`;
  return EnhancedComponent;
}

4. Consider Using Hooks Instead:

// Modern approach: Custom hooks
function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  return { count, increment };
}

// Usage
function Counter() {
  const { count, increment } = useCounter();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

// vs HOC approach
const CounterWithHOC = withCounter(Counter);

5. Handle Refs Properly:

// Good: Forward refs when needed
function withRef(WrappedComponent) {
  return React.forwardRef((props, ref) => {
    return <WrappedComponent {...props} ref={ref} />;
  });
}

Common Interview Questions

Q: What is a Higher-Order Component (HOC)?

  • A function that takes a component and returns a new component with enhanced functionality.

Q: What are the main benefits of using HOCs?

  • Code reuse, separation of concerns, and the ability to add functionality to components without modifying them.

Q: How do you create a basic HOC?

  • Create a function that takes a component as an argument and returns a new component with additional props or logic.

Q: What are some common use cases for HOCs?

  • Authentication, loading states, error boundaries, logging, and cross-cutting concerns.

Q: What are the limitations of HOCs?

  • Props drilling, wrapper hell, and potential naming conflicts.

Q: How do you handle refs in HOCs?

  • Use React.forwardRef to properly forward refs to the wrapped component.

Q: What’s the difference between HOCs and custom hooks?

  • HOCs are class-based patterns that wrap components, while custom hooks are function-based and can be used directly in components.

Q: How do you compose multiple HOCs?

  • Use a compose function or nest them: withA(withB(withC(Component))).

Q: What are some alternatives to HOCs?

  • Custom hooks, render props, and component composition.

Q: How do you debug HOCs?

  • Set proper display names and use React DevTools to inspect the component hierarchy.

Summary

  • HOCs are functions that take components and return enhanced components
  • Common patterns include withLoading, withErrorBoundary, and withAuthentication
  • Best practices include not mutating wrapped components and passing through props
  • Modern alternative is custom hooks for most use cases
  • Ref forwarding is important when the wrapped component needs refs
  • Composition allows combining multiple HOCs
  • Display names help with debugging
  • Understanding HOCs is crucial for advanced React patterns and legacy code

Interview angle

  • “Is the HOC pattern still relevant?” - mostly historical. Hooks cover the same need - sharing stateful logic - without wrapper nesting, prop-name collisions or the lost-static-methods problem. You still meet HOCs in older code and in libraries (connect, error-boundary wrappers, React.memo itself).
  • “What were the problems?” - wrapper hell in the tree, implicit props whose origin is invisible at the call site, ref forwarding needing extra work, and no type-safe way to express “adds these props” in older TypeScript.
  • “HOC, render prop, or hook?” - hook by default. Render props still have a place when the shared thing is rendering rather than logic. HOC when you must wrap a class component or conform to an existing library API.