Welcome back to the React Mastery Series!
In the previous article, we explored Authentication & Authorization and learned how enterprise React applications implement:
- JWT Authentication
- Protected Routes
- Role-Based Access Control (RBAC)
- Axios Interceptors
- Token Refresh
- Enterprise authentication architecture
Today, we'll move beyond React APIs and explore something that separates intermediate developers from senior engineers:
React Design Patterns
As applications grow, writing React code that simply works isn't enough.
We need code that is:
- Reusable
- Maintainable
- Scalable
- Easy to test
- Easy to extend
This is where design patterns come into play.
What are Design Patterns?
A design pattern is a proven solution to a commonly occurring software design problem.
Instead of reinventing solutions every time, developers use patterns that have already proven effective in large applications.
Think of them as reusable blueprints for structuring your code.
Why Design Patterns Matter
Imagine you're building an e-commerce application.
You'll have:
- Product Cards
- Shopping Cart
- Checkout
- User Profile
- Orders
- Wishlist
Without patterns:
Large Components
↓
Duplicate Logic
↓
Difficult Maintenance
↓
Frequent Bugs
With patterns:
Reusable Components
↓
Shared Logic
↓
Cleaner Code
↓
Easy Maintenance
Pattern 1 – Composition
React encourages Composition over Inheritance.
Instead of creating deeply nested class hierarchies, React allows components to be composed together.
Example:
type CardProps = {
children: React.ReactNode;
};
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
Usage:
<Card>
<h2>Bank Account</h2>
<p>Balance: $10,000</p>
</Card>
The Card component doesn't know what content it displays.
It simply provides a reusable layout.
Pattern 2 – Compound Components
Compound Components work together as a single unit.
Examples:
- Accordion
- Tabs
- Modal
- Select
- Menu
Instead of:
<Accordion
title="React"
content="React is a library."
/>
We compose smaller pieces.
<Accordion>
<Accordion.Header>
React
</Accordion.Header>
<Accordion.Body>
React is a library.
</Accordion.Body>
</Accordion>
Benefits:
- Flexible API
- Cleaner JSX
- Better readability
Real-World Example
Imagine building a banking dashboard card.
<Card>
<Card.Header>
Savings Account
</Card.Header>
<Card.Body>
AED 52,000
</Card.Body>
<Card.Footer>
Updated 2 mins ago
</Card.Footer>
</Card>
Each section has a clear responsibility.
Pattern 3 – Custom Hooks
Earlier in this series, we learned how to create Custom Hooks.
They are also a powerful design pattern.
Without Custom Hooks:
Dashboard
↓
Authentication Logic
↓
API Logic
↓
Loading Logic
Every page repeats the same code.
With Custom Hooks:
Dashboard
↓
useAuth()
↓
useAccounts()
↓
useNotifications()
Business logic becomes reusable.
Example:
function useUser() {
const [user, setUser] = useState(null);
useEffect(() => {
// Fetch user
}, []);
return user;
}
Now multiple components can reuse the same logic.
Pattern 4 – Render Props
A Render Prop is a function passed as a prop that returns JSX.
Example:
type DataLoaderProps = {
render: (data: string[]) => React.ReactNode;
};
function DataLoader({
render,
}: DataLoaderProps) {
const users = ["Alice", "Bob"];
return <>{render(users)}</>;
}
Usage:
<DataLoader
render={(users) => (
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
)}
/>
The component handles data fetching, while the parent decides how to display it.
Although Custom Hooks have replaced many Render Prop use cases, you'll still encounter this pattern in existing codebases.
Pattern 5 – Higher-Order Components (HOC)
A Higher-Order Component is a function that takes a component and returns a new component with additional behavior.
Component
↓
HOC
↓
Enhanced Component
Example:
function withLoading<T>(Component: React.ComponentType<T>) {
return function WrappedComponent(props: T & { loading: boolean }) {
if (props.loading) {
return <p>Loading...</p>;
}
return <Component {...props} />;
};
}
Usage:
const UserListWithLoading = withLoading(UserList);
Common HOC use cases:
- Authentication
- Authorization
- Logging
- Analytics
- Error handling
Modern React often prefers Custom Hooks, but HOCs remain important in many enterprise applications.
Pattern 6 – Provider Pattern
The Provider Pattern shares data across the application.
Example:
<AuthProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</AuthProvider>
Instead of passing props through many components:
App
↓
Dashboard
↓
Sidebar
↓
Profile
↓
Avatar
Any component can access shared data through Context.
Pattern 7 – Controlled Component Pattern
Many reusable components support both controlled and uncontrolled modes.
Example:
type ToggleProps = {
checked: boolean;
onChange: () => void;
};
function Toggle({checked, onChange,}: ToggleProps) {
return (
<button onClick={onChange}>
{checked ? "ON" : "OFF"}
</button>
);
}
The parent controls the component's state.
This pattern is widely used in UI libraries like Material UI and Ant Design.
Pattern 8 – Container & Presentational Components
Separate business logic from presentation.
Container:
function UserContainer() {
const users = useUsers();
return <UserList users={users} />;
}
Presentational Component:
type UserListProps = {
users: string[];
};
function UserList({users}: UserListProps) {
return (
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
);
}
Responsibilities:
| Container | Presentational |
|---|---|
| API Calls | UI Rendering |
| Business Logic | Layout |
| State Management | Display Data |
Pattern 9 – Feature-Based Architecture
Enterprise React applications organize code by features.
src
├── features
│ ├── auth
│ ├── dashboard
│ ├── accounts
│ ├── payments
│ └── transactions
Each feature contains:
accounts
├── components
├── hooks
├── pages
├── services
├── types
This keeps related files together and improves team collaboration.
Enterprise Example
Imagine an online banking application.
Dashboard
↓
Account Summary
↓
Transaction List
↓
Loan Details
↓
Investment Portfolio
Each module uses:
- Custom Hooks
- Shared Components
- Context Providers
- Feature Modules
- API Services
This architecture scales well even as the application grows.
Choosing the Right Pattern
| Problem | Recommended Pattern |
|---|---|
| Shared UI | Composition |
| Shared Logic | Custom Hooks |
| Global State | Provider Pattern |
| Flexible Components | Compound Components |
| Enhance Components | HOCs |
| Separate UI & Logic | Container/Presentational |
| Large Applications | Feature-Based Architecture |
Common Mistakes
1. Using Patterns Too Early
Not every component needs an advanced design pattern.
Start simple.
Refactor when duplication or complexity appears.
2. Creating Massive Components
Avoid components with:
- API calls
- Business logic
- Forms
- Tables
- Charts
- Navigation
all inside one file.
Break them into smaller, focused components.
3. Duplicating Business Logic
If the same logic appears in multiple places, consider moving it into:
- A Custom Hook
- A Service
- A Utility Function
4. Choosing Inheritance Over Composition
React is designed around composition.
Prefer combining small components rather than extending large ones.
Best Practices
- Prefer composition over inheritance.
- Keep components focused on a single responsibility.
- Reuse business logic through Custom Hooks.
- Organize code by feature for large applications.
- Use Compound Components for flexible APIs.
- Separate business logic from presentation.
- Introduce patterns only when they solve a real problem.
Key Takeaways
Today, we learned:
✅ Design patterns improve scalability and maintainability.
✅ Composition is the foundation of React architecture.
✅ Compound Components create expressive and flexible APIs.
✅ Custom Hooks are the preferred way to share business logic.
✅ Higher-Order Components are still relevant in many enterprise projects.
✅ Provider Pattern simplifies global state sharing.
✅ Feature-based architecture helps large teams organize code effectively.
Coming Next 🚀
In Day 29, we will explore:
React Error Handling – Error Boundaries, Logging & Resilient UI Design
We will learn:
- What happens when React crashes
- Error Boundaries
- Handling rendering errors
- Global error logging
- Async error handling
- Fallback UI
- Monitoring with tools like Sentry
- Building fault-tolerant React applications
By the end of the next article, you'll know how enterprise applications gracefully recover from unexpected errors instead of showing users a blank screen.
Happy Coding! 🚀
Top comments (1)
I particularly appreciated the section on Compound Components, as it highlights the flexibility and readability benefits of breaking down complex components into smaller, composable pieces. The example of the
Accordioncomponent demonstrates how this pattern can lead to a more flexible API and cleaner JSX. I've found that using Compound Components in my own projects has made it easier to reuse and maintain code, especially when working with complex interfaces. Have you found any challenges or trade-offs when implementing Compound Components in larger-scale applications, such as managing state or handling nested component updates?