Complete Guide to React Design Patterns: Build Scalable Applications

Table of Contents
Introduction to React Design Patterns
React has revolutionized frontend development with its component-based architecture. However, as applications grow in complexity, developers need proven patterns to maintain scalability, reusability, and maintainability.
Design patterns in React provide solutions to common problems, helping you:
- 🏗️ Structure complex component hierarchies
- ♻️ Maximize code reuse
- ⚡ Improve performance
- 🧩 Create more maintainable codebases
Why React Design Patterns Matter
- Standardized Solutions: Solve common problems with proven approaches
- Team Alignment: Create shared vocabulary among developers
- Future-Proofing: Make your codebase easier to maintain and extend
- Performance Optimization: Many patterns include built-in optimizations
1. Container/Presentational Pattern
Separates concerns between logic (Container) and UI (Presentational) components.
❌ Mixed Concerns Component
// Bad: Mixes logic and presentation
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Problems: Hard to test, reuse, and maintain as component grows.
✅ Container/Presentational Implementation
// Presentational Component
function UserListUI({ users, loading }) {
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Container Component
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return <UserListUI users={users} loading={loading} />;
}
Benefits: Better separation of concerns, easier testing, and improved reusability.
2. Hooks Pattern
Encapsulate and reuse stateful logic with custom hooks.
❌ Duplicated Logic Across Components
// ComponentA.js
function ComponentA() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchData('https://api.example.com/dataA')
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// ...
}
// ComponentB.js - Same logic repeated
function ComponentB() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchData('https://api.example.com/dataB')
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// ...
}
✅ Custom Hook Solution
// useFetch.js - Custom hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchData(url)
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// ComponentA.js
function ComponentA() {
const { data, loading, error } = useFetch('https://api.example.com/dataA');
// ...
}
// ComponentB.js
function ComponentB() {
const { data, loading, error } = useFetch('https://api.example.com/dataB');
// ...
}
Benefits: Eliminates code duplication, centralizes logic, and simplifies components.
3. Compound Components Pattern
Components that work together to form a complete UI.
❌ Monolithic Component
// Bad: One large component with many props
function Dropdown({
options,
selected,
onSelect,
showSearch,
multiSelect,
// ...many more props
}) {
// Complex implementation handling all cases
return (
<div className="dropdown">
{/* Complex JSX */}
</div>
);
}
// Usage:
<Dropdown
options={[...]}
selected={selected}
onSelect={handleSelect}
showSearch={true}
multiSelect={false}
/>
✅ Compound Components Implementation
// Compound Dropdown Components
function Dropdown({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="dropdown">
{React.Children.map(children, child => {
return React.cloneElement(child, { isOpen, setIsOpen });
})}
</div>
);
}
function DropdownToggle({ children, isOpen, setIsOpen }) {
return (
<button onClick={() => setIsOpen(!isOpen)}>
{children}
</button>
);
}
function DropdownMenu({ children, isOpen }) {
return isOpen ? <div className="menu">{children}</div> : null;
}
function DropdownItem({ children, onClick }) {
return <div className="item" onClick={onClick}>{children}</div>;
}
// Usage:
<Dropdown>
<DropdownToggle>Select Option</DropdownToggle>
<DropdownMenu>
<DropdownItem onClick={() => console.log('Option 1')}>
Option 1
</DropdownItem>
<DropdownItem onClick={() => console.log('Option 2')}>
Option 2
</DropdownItem>
</DropdownMenu>
</Dropdown>
Benefits: More flexible API, better separation of concerns, and improved readability.
4. Render Props Pattern
Share code between components using a prop whose value is a function.
✅ Render Props Implementation
// MouseTracker.js
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
}
// Usage:
<MouseTracker
render={({ x, y }) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)}
/>
// Alternative usage with children:
<MouseTracker>
{({ x, y }) => (
<p>
Current position: {x}, {y}
</p>
)}
</MouseTracker>
When to use: When you need to share behavior while letting the consumer control the rendering.
5. Higher-Order Components (HOC)
A function that takes a component and returns a new component with enhanced functionality.
✅ HOC Implementation
// withLoading.js
function withLoading(Component) {
return function EnhancedComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
};
}
// UserProfile.js
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Enhanced component
const UserProfileWithLoading = withLoading(UserProfile);
// Usage:
<UserProfileWithLoading
isLoading={loading}
user={currentUser}
/>
When to use: When you need to add common functionality to multiple components.
6. Provider Pattern
Share data across multiple components without prop drilling.
✅ Context API Implementation
// ThemeContext.js
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// App.js
function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
<Footer />
</ThemeProvider>
);
}
// Any child component
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
Benefits: Eliminates prop drilling, centralizes state management, and improves component reusability.
Conclusion and Best Practices
Implementing these React design patterns will significantly improve your application architecture:
Pattern Selection Guide
- Container/Presentational: When you need to separate logic from presentation
- Hooks: For reusable stateful logic
- Compound Components: For flexible, customizable UI components
- Render Props: When you need to share behavior with rendering flexibility
- HOC: For cross-cutting concerns across multiple components
- Provider: For global state management
Key Recommendations
- Start simple - Don't over-engineer early in development
- Be consistent - Use the same pattern for similar problems
- Combine patterns - Many patterns work well together (e.g., Hooks + Provider)
- Consider tradeoffs - Each pattern has strengths and appropriate use cases
By mastering these patterns, you'll be able to build React applications that are more maintainable, scalable, and enjoyable to work with.