Advanced React Patterns: Compound Components, Render Props, and Higher-Order Components
React has revolutionized the way we build user interfaces, offering a component-based architecture that promotes reusability and modularity. As applications grow in complexity, developers often encounter scenarios where basic component composition falls short. This is where advanced React patterns come into play. In this article, we'll explore three powerful patterns: Compound Components, Render Props, and Higher-Order Components (HOCs).
- Compound Components
Compound Components is a pattern that allows you to create a set of components that work together to form a cohesive unit. This pattern is particularly useful when you have a complex component with multiple interdependent parts.
The key idea behind Compound Components is to provide a flexible and declarative way to compose a component's structure while maintaining an implicit state sharing between the parent and child components.
Let's look at an example of a simple accordion using the Compound Components pattern:
import React, { createContext, useState, useContext } from 'react';
const AccordionContext = createContext();
const Accordion = ({ children }) => {
const [openIndex, setOpenIndex] = useState(null);
return (
<AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
};
const AccordionItem = ({ children, index }) => {
const { openIndex, setOpenIndex } = useContext(AccordionContext);
const isOpen = openIndex === index;
const toggleItem = () => {
setOpenIndex(isOpen ? null : index);
};
return (
<div className="accordion-item">
{React.Children.map(children, child =>
React.cloneElement(child, { isOpen, toggleItem })
)}
</div>
);
};
const AccordionHeader = ({ children, toggleItem }) => (
<div className="accordion-header" onClick={toggleItem}>
{children}
</div>
);
const AccordionContent = ({ children, isOpen }) => (
<div className={`accordion-content ${isOpen ? 'open' : ''}`}>
{children}
</div>
);
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Content = AccordionContent;
export default Accordion;
Now, you can use this Accordion component like this:
<Accordion>
<Accordion.Item index={0}>
<Accordion.Header>Section 1</Accordion.Header>
<Accordion.Content>Content for section 1</Accordion.Content>
</Accordion.Item>
<Accordion.Item index={1}>
<Accordion.Header>Section 2</Accordion.Header>
<Accordion.Content>Content for section 2</Accordion.Content>
</Accordion.Item>
</Accordion>
The Compound Components pattern provides a clean and intuitive API for complex components, allowing for flexible composition and implicit state sharing.
- Render Props
The Render Props pattern involves passing a function as a prop to a component, which the component then uses to render its content. This pattern is particularly useful for sharing behavior between components without tightly coupling them.
Here's an example of a MouseTracker component using the Render Props pattern:
import React, { useState, useEffect } from 'react';
const MouseTracker = ({ render }) => {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setMousePosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return render(mousePosition);
};
// Usage
const App = () => (
<MouseTracker
render={({ x, y }) => (
<div>
The current mouse position is ({x}, {y})
</div>
)}
/>
);
The Render Props pattern allows for great flexibility in how the tracked mouse position is used and displayed. You can easily swap out the render function to create different visualizations or behaviors based on the mouse position.
- Higher-Order Components (HOCs)
Higher-order components are functions that take a component as an argument and return a new component with some added functionality. HOCs are a powerful way to reuse component logic across multiple components.
Here's an example of an HOC that adds loading functionality to a component:
import React, { useState } from 'react';
const withLoading = (WrappedComponent) => {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
};
// A simple component
const UserList = ({ users }) => (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
// Enhance UserList with loading functionality
const UserListWithLoading = withLoading(UserList);
// Usage
const App = () => {
const [isLoading, setIsLoading] = useState(true);
const [users, setUsers] = useState([]);
// Simulating an API call
useEffect(() => {
setTimeout(() => {
setUsers([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]);
setIsLoading(false);
}, 2000);
}, []);
return <UserListWithLoading isLoading={isLoading} users={users} />;
};
HOCs are particularly useful for cross-cutting concerns like logging, access control, or in this case, handling loading states.
Comparing the Patterns
Each of these patterns has its strengths and use cases:
Compound Components are excellent for creating flexible, composable component APIs. They're particularly useful for components with multiple, interdependent parts.
Render Props offer a high degree of flexibility and are great for sharing behavior between components. They make it easy to change the rendering logic without modifying the component itself.
Higher-order components are powerful for adding reusable functionality to components. They're particularly useful for cross-cutting concerns that apply to many components.
When to Use Each Pattern
Use Compound Components when you have a complex component with multiple related parts that need to share state implicitly.
Use Render Props when you want to share behavior between components and need maximum flexibility in how that behavior is rendered.
Use Higher-Order Components when you have functionality that needs to be added to multiple unrelated components.