React Patterns
Compound components, render props, HOCs, and advanced custom hook patterns
React Design Patterns
As React applications grow, repeating logic across components becomes a problem. React patterns are proven solutions for sharing behavior, creating flexible APIs, and keeping code maintainable. Let's explore the most important patterns used in production.
Compound Components
Compound components share implicit state among a group of related components. They give users full control over rendering while keeping the logic encapsulated. Think of <select> and <option> in HTML.
import { createContext, useContext, useState } from 'react';
// Shared context
const AccordionContext = createContext();
function Accordion({ children, defaultOpen = null }) {
const [openItem, setOpenItem] = useState(defaultOpen);
return (
<AccordionContext.Provider value={{ openItem, setOpenItem }}>
<div className="border rounded-lg divide-y">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ id, children }) {
return <div>{children}</div>;
}
function AccordionTrigger({ id, children }) {
const { openItem, setOpenItem } = useContext(AccordionContext);
const isOpen = openItem === id;
return (
<button
onClick={() => setOpenItem(isOpen ? null : id)}
className="w-full p-4 text-left flex justify-between items-center"
>
{children}
<span>{isOpen ? '▲' : '▼'}</span>
</button>
);
}
function AccordionContent({ id, children }) {
const { openItem } = useContext(AccordionContext);
if (openItem !== id) return null;
return <div className="p-4 bg-gray-50">{children}</div>;
}
// Attach sub-components
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;
// Usage — clean, flexible API
function FAQ() {
return (
<Accordion defaultOpen="q1">
<Accordion.Item id="q1">
<Accordion.Trigger id="q1">What is React?</Accordion.Trigger>
<Accordion.Content id="q1">React is a UI library.</Accordion.Content>
</Accordion.Item>
<Accordion.Item id="q2">
<Accordion.Trigger id="q2">Why use React?</Accordion.Trigger>
<Accordion.Content id="q2">Component model, ecosystem.</Accordion.Content>
</Accordion.Item>
</Accordion>
);
}
Custom Hooks for Shared Logic
The modern replacement for HOCs and render props — extract reusable stateful logic into custom hooks:
// useLocalStorage.js — Persist state to localStorage
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// useMediaQuery.js — Responsive breakpoints in JS
export function useMediaQuery(query) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
// useDebounce.js — Debounce rapidly changing values
export function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage
function SearchPage() {
const [query, setQuery] = useLocalStorage('search', '');
const debouncedQuery = useDebounce(query, 500);
const isMobile = useMediaQuery('(max-width: 768px)');
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery);
}, [debouncedQuery]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{isMobile ? <MobileResults /> : <DesktopResults />}
</div>
);
}
Render Props Pattern
A component receives a function as a prop and calls it with data — gives full rendering control to the consumer:
// MouseTracker using render props
function MousePosition({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// Usage — you decide how to display the data
function App() {
return (
<div>
<MousePosition
render={({ x, y }) => (
<p>Mouse is at ({x}, {y})</p>
)}
/>
<MousePosition
render={({ x, y }) => (
<div
className="w-4 h-4 bg-red-500 rounded-full fixed"
style={{ left: x, top: y }}
/>
)}
/>
</div>
);
}
// Modern alternative: use a custom hook instead
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return position;
}
Higher-Order Components (HOC)
A function that takes a component and returns a new component with enhanced behavior. Still used in some libraries:
// withAuth HOC — protects routes
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
const ProtectedSettings = withAuth(Settings);
// In routes
<Route path="/dashboard" element={<ProtectedDashboard />} />
<Route path="/settings" element={<ProtectedSettings />} />
Container / Presentational Pattern
Separate data fetching (container) from rendering (presentational) for cleaner code:
// Container — handles data logic
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
useEffect(() => {
fetchUsers().then(data => { setUsers(data); setLoading(false); });
}, []);
const filteredUsers = users.filter(u =>
u.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<UserList
users={filteredUsers}
loading={loading}
filter={filter}
onFilterChange={setFilter}
/>
);
}
// Presentational — pure rendering, easy to test
function UserList({ users, loading, filter, onFilterChange }) {
if (loading) return <Skeleton />;
return (
<div>
<input
value={filter}
onChange={e => onFilterChange(e.target.value)}
placeholder="Search users..."
/>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
{users.length === 0 && <li>No users found</li>}
</ul>
</div>
);
}
Controlled vs Uncontrolled Components
Build components that work in both controlled (parent manages state) and uncontrolled (self-managed) modes:
function Toggle({ value: controlledValue, onChange, defaultValue = false }) {
// Internal state (uncontrolled mode)
const [internalValue, setInternalValue] = useState(defaultValue);
// Use controlled value if provided, otherwise internal
const isControlled = controlledValue !== undefined;
const isOn = isControlled ? controlledValue : internalValue;
function handleToggle() {
if (isControlled) {
onChange?.(!controlledValue);
} else {
setInternalValue(prev => {
onChange?.(!prev);
return !prev;
});
}
}
return (
<button
onClick={handleToggle}
className={`px-4 py-2 rounded ${isOn ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
{isOn ? 'ON' : 'OFF'}
</button>
);
}
// Uncontrolled usage
<Toggle defaultValue={true} onChange={v => console.log(v)} />
// Controlled usage
const [darkMode, setDarkMode] = useState(false);
<Toggle value={darkMode} onChange={setDarkMode} />
💡 When to Use Which Pattern
- • Custom Hooks: First choice for sharing stateful logic (replaces most HOCs and render props)
- • Compound Components: Building component libraries with flexible APIs (tabs, dropdowns, accordions)
- • Render Props: When a hook isn't sufficient and you need rendering flexibility
- • HOCs: Cross-cutting concerns like auth guards, logging, analytics wrappers
- • Container/Presentational: Clean separation of data and UI in complex features