Technology · React
React Component Patterns
Build scalable component architectures with established design patterns.
TL;DR
- 01Use compound components to create flexible component APIs.
- 02Use render props for shared logic without nesting.
- 03Use higher-order components to wrap and enhance components.
Compound Components
- Build components that work together as a unit.
function Tabs() { const [active, setActive] = useState(0); return ( <TabsContext.Provider value={{ active, setActive }}> {/* Children use context */} </TabsContext.Provider> ); } Tabs.TabList = function TabList({ children }) { return <div>{children}</div>; }; Tabs.Tab = function Tab({ index, children }) { const { active, setActive } = useContext(TabsContext); return ( <button onClick={() => setActive(index)}> {children} </button> ); }; Tabs.Panel = function Panel({ index, children }) { const { active } = useContext(TabsContext); return active === index && <div>{children}</div>; }; // Usage <Tabs> <Tabs.TabList> <Tabs.Tab index={0}>Tab 1</Tabs.Tab> <Tabs.Tab index={1}>Tab 2</Tabs.Tab> </Tabs.TabList> <Tabs.Panel index={0}>Content 1</Tabs.Panel> <Tabs.Panel index={1}>Content 2</Tabs.Panel> </Tabs> - Compound components allow flexible nesting and customization.
Render Props
- Pass a function as a prop to share logic.
function MouseTracker({ children }) { const [position, setPosition] = useState({ x: 0, y: 0 }); function handleMouseMove(event) { setPosition({ x: event.clientX, y: event.clientY }); } return ( <div onMouseMove={handleMouseMove}> {children(position)} </div> ); } // Usage <MouseTracker> {(position) => ( <p>Mouse position: {position.x}, {position.y}</p> )} </MouseTracker> - Render props are more flexible than HOCs for multiple consumers.
function DataFetcher({ url, children }) { const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then(r => r.json()) .then(setData) .catch(setError); }, [url]); return children({ data, error }); }
Higher-Order Components
- Wrap a component to add behavior or props.
function withAuthentication(Component) { return function ProtectedComponent(props) { const { isAuthenticated } = useAuth(); if (!isAuthenticated) { return <div>Please log in</div>; } return <Component {...props} />; }; } const ProtectedPage = withAuthentication(Page); - Use HOCs to share state logic across components.
function withToggle(Component) { return function ToggledComponent(props) { const [isOpen, setIsOpen] = useState(false); return ( <Component {...props} isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} /> ); }; }
Custom Hooks Pattern
- Extract logic into custom hooks for reusability.
function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(url) .then(r => r.json()) .then(setData) .finally(() => setLoading(false)); }, [url]); return { data, loading }; } // Usage in any component function MyComponent() { const { data, loading } = useFetch('/api/data'); return loading ? <p>Loading...</p> : <p>{data}</p>; } - Custom hooks are the modern approach to code sharing.
Container/Presentational Pattern
- Separate logic (container) from presentation (component).
// Container: handles logic and state function UserListContainer() { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') .then(r => r.json()) .then(setUsers); }, []); return <UserList users={users} />; } // Presentational: pure UI component function UserList({ users }) { return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } - Makes components easier to test and reuse.
Tip: Use custom hooks as the first choice for sharing logic — they're simpler than HOCs and more flexible than render props.
Warning: Avoid deeply nested render props or multiple HOCs — this creates "wrapper hell" that's hard to debug.