Mastering Higher-Order Components (HOCs) in React: Best Practices and Patterns

Sumeet Panchal
4 min readNov 9, 2024

--

Higher-order components (HOCs) are a powerful design pattern in React that enhances components by wrapping them in reusable logic. Creating HOCs allows you to separate concerns, share logic across components, and keep code DRY. However, to truly leverage HOCs, following best practices and avoiding common pitfalls is essential.

In this post, we’ll explore best practices for writing effective HOCs, including preserving the original component, passing props correctly, maximizing composability, and avoiding specific caveats.

What is an HOC?

An HOC is a function that takes a component and returns a new, enhanced version of it. For example:

const withEnhancement = (WrappedComponent) => {
return (props) => <WrappedComponent {...props} />;
};

This basic HOC takes a component as input and returns a new version of it, passing through any props the original component needs.

Best Practices for Writing Effective HOCs

1. Don’t Mutate the Original Component

When creating an HOC, it’s important to return a new component rather than altering the original component directly. Mutating the original component can lead to unexpected side effects, especially if it’s reused elsewhere.

Example: Adding a Click Counter to a Button

// Original component
const Button = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);

// HOC to add click count
const withClickCounter = (WrappedComponent) => {
return class extends React.Component {
state = { count: 0 };

handleClick = () => {
this.setState({ count: this.state.count + 1 });
};

render() {
return (
<div>
<WrappedComponent onClick={this.handleClick} {...this.props} />
<p>Button clicked {this.state.count} times</p>
</div>
);
}
};
};

const EnhancedButton = withClickCounter(Button);

Here, withClickCounter creates a new version of Button that tracks clicks without altering the original. This ensures Button can still be used without the counter elsewhere.

2. Pass Unrelated Props Through to the Wrapped Component

HOCs should pass all unrelated props to the wrapped component. This allows the HOC to stay flexible, passing along props that may not directly relate to its own logic.

Example: Passing Props in an HOC with User Data

const withUserData = (WrappedComponent) => {
return class extends React.Component {
state = { user: { name: "Jane Doe", age: 25 } };

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

const Profile = ({ user, theme }) => (
<div style={{ color: theme }}>
<h2>Name: {user.name}</h2>
<p>Age: {user.age}</p>
</div>
);

const EnhancedProfile = withUserData(Profile);

By spreading ...this.props, the HOC ensures that any additional props (theme in this case) are passed down, keeping EnhancedProfile flexible and composable with other HOCs or components.

3. Maximize Composability with compose

Composability is key to creating reusable and modular HOCs. compose from libraries like Redux enables you to combine multiple HOCs in a readable, structured way, where each HOC progressively enhances the component.

Example: Composing Multiple HOCs

import { compose } from 'redux';

const withViewportTracking = (WrappedComponent) => {
return class extends React.Component { /* Component logic */ };
};

const withErrorHandling = (WrappedComponent) => {
return class extends React.Component { /* Error handling logic */ };
};

const withReduxConnection = connect((state) => ({
data: state.data,
}));

const enhance = compose(
withViewportTracking,
withErrorHandling,
withReduxConnection
);

const EnhancedComponent = enhance(MyComponent);

In this example, compose chains withViewportTracking, withErrorHandling, and withReduxConnection to create EnhancedComponent with all three enhancements applied.

Caveats to Keep in Mind

While HOCs offer powerful composition, they come with some caveats.

Avoid Using HOCs Inside Render Functions

Creating an HOC inside a render function causes a new HOC to be created on every render, leading to performance issues. Define HOCs outside render functions to avoid unnecessary re-renders.

// Don't do this inside render functions
const EnhancedChild = withEnhancement(ChildComponent);

// Instead, define outside
const EnhancedChild = withEnhancement(ChildComponent);
const ParentComponent = () => <EnhancedChild />;

Refs Aren’t Passed Through

HOCs don’t automatically pass ref down to the wrapped component. If you need the ref, use React.forwardRef to explicitly forward it.

Example with React.forwardRef

const withLogging = (WrappedComponent) => {
return React.forwardRef((props, ref) => {
console.log("Rendering with props:", props);
return <WrappedComponent ref={ref} {...props} />;
});
};

class MyButton extends React.Component {
focus() {
console.log("Button focused!");
}
render() {
return <button {...this.props}>Click Me</button>;
}
}

const EnhancedButton = withLogging(MyButton);

const Parent = () => {
const buttonRef = React.useRef();

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

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

Here, React.forwardRef allows Parent to use a ref on EnhancedButton, forwarding it to MyButton where it can be accessed.

Conclusion

Higher-Order Components are a powerful tool in React, enabling code reuse and separation of concerns. By following best practices like preserving the original component, passing through unrelated props, maximizing composability, and handling caveats around rendering and refs, you can build efficient, flexible HOCs. With these strategies, HOCs become a valuable pattern in creating modular, maintainable React applications.

--

--

Sumeet Panchal
Sumeet Panchal

Written by Sumeet Panchal

Programming enthusiast specializing in Android and React Native, passionate about crafting intuitive mobile experiences and exploring innovative solutions.

No responses yet