React Composition
Master React patterns like compound components, render props, and higher-order components for clean reusable patterns.
TL;DR
- 01Use compound components to create flexible component APIs.
- 02Use render props to share logic through children functions.
- 03Use higher-order components to wrap and enhance existing components.
Tips
- 01Use custom hooks instead of render props or HOCs for new code, since hooks are simpler and easier to understand.
Warnings
- 01Deeply nested HOCs create hard-to-debug code — prefer flat composition using multiple custom hooks instead.
Compound Components
- Build components that work together as a group with shared state.
const Accordion = ({ children }) => { const [active, setActive] = useState(null); return ( <AccordionContext.Provider value={{ active, setActive }}> {children} </AccordionContext.Provider> ); }; const AccordionItem = ({ id, title, children }) => { const { active, setActive } = useContext(AccordionContext); return ( <div> <button onClick={() => setActive(id)}>{title}</button> {active === id && <div>{children}</div>} </div> ); }; <Accordion> <AccordionItem id="1" title="Section 1">Content 1</AccordionItem> <AccordionItem id="2" title="Section 2">Content 2</AccordionItem> </Accordion> - Compound components are flexible because the child components control their appearance.
- They share state through Context instead of passing props through every level.
- Use this pattern for tightly coupled components like form fields and labels.
Render Props
- Pass a function as a prop to let child components decide what to render.
const MouseTracker = ({ render }) => { const [pos, setPos] = useState({ x: 0, y: 0 }); const handleMouseMove = (e) => { setPos({ x: e.clientX, y: e.clientY }); }; return ( <div onMouseMove={handleMouseMove}> {render(pos)} </div> ); }; <MouseTracker render={({ x, y }) => ( <p>Mouse at {x}, {y}</p> )} /> - The render function receives data and returns JSX to display.
- This lets you reuse logic without creating a wrapper component.
- Render props are flexible because the caller decides what to render.
- The
childrenfunction is a special render prop pattern.<MouseTracker> {({ x, y }) => <p>Position: {x}, {y}</p>} </MouseTracker>
Higher-Order Components (HOC)
- Wrap a component to add or modify its behavior.
const withAuth = (Component) => { return (props) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { checkAuth().then(u => { setUser(u); setLoading(false); }); }, []); if (loading) return <p>Loading...</p>; if (!user) return <p>Not authenticated</p>; return <Component user={user} {...props} />; }; }; const Dashboard = ({ user }) => <h1>Welcome {user.name}</h1>; const ProtectedDashboard = withAuth(Dashboard); - HOCs add props, logic, or wrappers to existing components.
- They're useful for cross-cutting concerns like authentication or theming.
- Avoid deeply nested HOCs — use composition instead.
When to Use Each Pattern
- Use compound components when building a cohesive component suite.
<Form> <Form.Field name="email" /> <Form.Field name="password" /> <Form.Submit>Login</Form.Submit> </Form> - Use render props to share logic between unrelated components.
<DataFetcher url="/api/users" render={data => ( <UserList users={data} /> )} /> - Use HOCs to enhance existing components with logic.
const enhancedComponent = withDataFetching(MyComponent); - Prefer composition over deeply nested patterns for readability.
Modern Alternatives
- Custom hooks often replace render props and HOCs for sharing logic.
function useMousePosition() { const [pos, setPos] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []); return pos; } function MyComponent() { const pos = useMousePosition(); return <p>Position: {pos.x}, {pos.y}</p>; } - Custom hooks are simpler and more flexible than render props or HOCs.
- Use hooks as your first choice for sharing logic in modern React.
- Composition patterns are still useful for component APIs and layout.
FAQ
Use compound components when you want a flexible, declarative API where parent and child components share implicit state (like a Tabs/Tab pair). Use render props when you need to inject dynamic content or logic into a single component that consumers control directly.
Extract the logic into a custom hook and call it in each component that needs it. This is the modern recommended approach — it avoids wrapper components entirely and keeps the component tree flat.
An HOC wraps a component and returns a new one, adding behavior at the component level; it shows up in the React tree and can obscure the component hierarchy. A custom hook shares logic at the function level without adding any components to the tree, making it easier to trace and debug.
Wrapper hell happens when multiple HOCs are stacked around a single component, making the component tree and props difficult to follow. Refactor each HOC's logic into a separate custom hook and call them directly inside the component instead.
Use React context inside the compound component — the parent holds the shared state and provides it via a context, and each child reads from that context directly. This keeps the public API clean while avoiding the need to pass props through every intermediate child.