Higher-Order Components (HOCs) Best Practices

Loading

Higher-Order Components (HOCs) are advanced React patterns for reusing component logic. Here are the key best practices to follow when implementing HOCs:

1. Naming Conventions

  • Prefix HOC names: Use a prefix like with (e.g., withAuth, withLoading)
  • Display name: Set a proper display name for debugging
function withExample(Component) {
  function WithExample(props) {
    /* ... */
  }

  // Set display name
  WithExample.displayName = `WithExample(${Component.displayName || Component.name || 'Component'})`;
  return WithExample;
}

2. Pass Unrelated Props Through

Always pass down props that aren’t consumed by the HOC:

function withData(WrappedComponent) {
  return function WithData(props) {
    const [data, setData] = useState(null);

    // Filter out HOC-specific props and pass the rest
    const { extraProp, ...passThroughProps } = props;

    return (
      <WrappedComponent 
        data={data} 
        {...passThroughProps} 
      />
    );
  }
}

3. Don’t Mutate the Original Component

Instead of modifying the input component, create a new one:

// Bad - Mutates original component
function badHOC(Component) {
  Component.defaultProps = { /* ... */ };
  return Component;
}

// Good - Creates new component
function goodHOC(Component) {
  return function EnhancedComponent(props) {
    return <Component {...props} />;
  }
}

4. Maximize Composability

Design HOCs to work well when composed together:

// Can be composed like this:
const EnhancedComponent = withRouter(
  withAuth(
    withLoading(MyComponent)
  )
);

// Or using compose utility (from Redux, Ramda, etc.):
const enhance = compose(
  withRouter,
  withAuth,
  withLoading
);
const EnhancedComponent = enhance(MyComponent);

5. Use Static Methods Correctly

Copy static methods from wrapped component:

function withFeature(WrappedComponent) {
  class WithFeature extends React.Component {
    /* ... */
  }

  // Copy static methods
  const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';

  WithFeature.displayName = `WithFeature(${wrappedComponentName})`;
  WithFeature.staticMethod = WrappedComponent.staticMethod;

  return WithFeature;
}

// Or use hoist-non-react-statics package
import hoistNonReactStatic from 'hoist-non-react-statics';

function withFeature(WrappedComponent) {
  class WithFeature extends React.Component {
    /* ... */
  }

  hoistNonReactStatic(WithFeature, WrappedComponent);
  return WithFeature;
}

6. Forward Refs

Use React.forwardRef to maintain ref access to wrapped components:

function withLogging(WrappedComponent) {
  class WithLogging extends React.Component {
    /* ... */
  }

  return React.forwardRef((props, ref) => {
    return <WithLogging {...props} forwardedRef={ref} />;
  });
}

7. Type Safety with TypeScript

When using TypeScript, properly type your HOCs:

interface WithLoadingProps {
  isLoading: boolean;
}

function withLoading<P extends object>(
  Component: React.ComponentType<P>
): React.ComponentType<P & WithLoadingProps> {
  return function WithLoading({ isLoading, ...props }: P & WithLoadingProps) {
    return isLoading ? <Spinner /> : <Component {...props as P} />;
  };
}

8. Document Expected Props

Clearly document any props the HOC injects or requires:

/**
 * HOC that provides authentication
 * @param {React.Component} WrappedComponent - Component to enhance
 * @returns {React.Component} - New component with these props:
 *   - isAuthenticated {boolean}
 *   - user {object}
 *   - login {function}
 *   - logout {function}
 */
function withAuth(WrappedComponent) {
  /* implementation */
}

9. Performance Considerations

  • Avoid creating HOCs inside render methods (creates new component types each render)
  • Implement shouldComponentUpdate/PureComponent/memo where appropriate
  • Consider using hooks instead for simpler cases (HOCs can be replaced with custom hooks in many situations)

10. Testing

Make sure to test both:

  • The HOC itself
  • Components wrapped with the HOC
// Test example
test('withLoading shows spinner when loading', () => {
  const TestComponent = () => <div>Content</div>;
  const WrappedComponent = withLoading(TestComponent);
  const { getByTestId } = render(<WrappedComponent isLoading={true} />);
  expect(getByTestId('spinner')).toBeInTheDocument();
});

When to Use HOCs vs Hooks

While Hooks have replaced many HOC use cases, HOCs are still useful when:

  • You need to manipulate the rendered component (e.g., adding wrappers)
  • You need to maintain compatibility with class components
  • You’re working with existing codebases that heavily use HOCs

Remember that HOCs create a layer of indirection, so prefer custom hooks when possible for simpler logic reuse.

Leave a Reply

Your email address will not be published. Required fields are marked *