Design patterns represent time-tested solutions to common problems in software development. Understanding these React Design Patterns is crucial for building maintainable, scalable, and performant applications in the React ecosystem. As the React library has evolved—particularly with the introduction of Hooks in React 16.8—so too have the patterns and best practices for React development.
This guide explores the most effective React Design Patterns in modern React development. Whether building a small application or a complex enterprise system, these patterns will help you write cleaner, more maintainable code while avoiding common pitfalls.
React’s popularity stems partly from its flexibility—it doesn’t force developers into rigid structures. However, this flexibility can be a double-edged sword. Without established patterns, React applications can quickly become unwieldy. The React Design Patterns outlined in this article provide structure and consistency while preserving React’s flexibility.
Modern React development favors functional components with Hooks over class components.
// Modern functional approach
function UserProfile({ name, email }) {
const [isEditing, setIsEditing] = useState(false);
return (
<div className="user-profile">
<h2>{name}</h2>
<p>{email}</p>
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? 'Cancel' : 'Edit'}
</button>
</div>
);
}
Functional components are more concise, easier to test, and better optimized for modern tooling.
This pattern separates data handling from UI rendering.
// Container component (handles data)
function UserProfileContainer() {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUserData().then(data => {
setUserData(data);
setLoading(false);
});
}, []);
if (loading) return <LoadingSpinner />;
return <UserProfileView user={userData} />;
}
// Presentational component (handles UI)
function UserProfileView({ user }) {
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Custom Hooks allow you to extract and reuse stateful logic between components.
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
// Usage
function MouseDisplay() {
const { x, y } = useMousePosition();
return <p>Mouse position: {x}, {y}</p>;
}
Custom Hooks are more intuitive than older patterns like Higher-Order Components (HOCs) or render props, which often led to wrapper hell and prop collision issues.
For simpler components, local state using useState or useReducer is often sufficient:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
</div>
);
}
For more complex state logic, useReducer provides a Redux-like approach within a component.
For a state that needs to be accessed by many components, the Context API provides a solution:
// Create and use a theme context
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for using the theme
function useTheme() {
return useContext(ThemeContext);
}
// Usage in components
function ThemedButton() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className={`btn-${theme}`}
>
Toggle Theme
</button>
);
}
For complex applications, external libraries like Redux Toolkit, Zustand, or Jotai offer additional benefits:
// Zustand example (simpler than Redux)
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 }))
}));
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
Libraries like React Query or SWR simplify data fetching and caching:
// React Query example
function TodoList() {
const { data: todos, isLoading } = useQuery('todos', fetchTodos);
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
These libraries handle caching, background refetching, and loading states automatically.
Related read: Boost Performance with React-Query: Managing Async State
// Basic button component
function Button({ children, className, ...props }) {
return (
<button className={`button ${className || ''}`} {...props}>
{children}
</button>
);
}
// Specialized buttons using composition
function PrimaryButton(props) {
return <Button className="primary" {...props} />;
}
function IconButton({ icon, children, ...props }) {
return (
<Button {...props}>
{icon} {children}
</Button>
);
}
The children prop allows for flexible composition:
function Card({ title, children }) {
return (
<div className="card">
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<Card title="User Profile">
<p>Name: John Doe</p>
<button>Edit Profile</button>
</Card>
);
}
Create component families for better organization:
function Dialog({ children }) {
return <div className="dialog">{children}</div>;
}
Dialog.Header = function({ children }) {
return <div className="dialog-header">{children}</div>;
};
Dialog.Body = function({ children }) {
return <div className="dialog-body">{children}</div>;
};
Dialog.Footer = function({ children }) {
return <div className="dialog-footer">{children}</div>;
};
// Usage
function ConfirmDialog() {
return (
<Dialog>
<Dialog.Header>Confirm Action</Dialog.Header>
Dialog.Body>Are you sure?</Dialog.Body>
<Dialog.Footer>
<button>Cancel</button>
<button>Confirm</button>
</Dialog.Footer>
</Dialog>
);
}
Use React’s memoization tools to prevent unnecessary re-renders:
// Component memoization
const MemoizedComponent = React.memo(function ExpensiveComponent({ data }) {
// Expensive rendering logic
return <div>{/* Rendered content */}</div>;
});
// Value memoization
function DataGrid({ items, filterText }) {
// Only recalculates when dependencies change
const filteredItems = useMemo(() =>
items.filter(item => item.name.includes(filterText)),
[items, filterText]
);
return (
<div>
{filteredItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// Function memoization
function ParentComponent() {
const [count, setCount] = useState(0);
// Function only recreated when dependencies change
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ChildComponent onButtonClick={handleClick} />
</div>
);
}
React’s React.lazy and Suspense enable code splitting:
import React, { Suspense, lazy } from 'react';
// Lazy-loaded component
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
</div>
);
}
For long lists, virtualization renders only visible items:
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<FixedSizeList
height={400}
width={300}
itemCount={items.length}
itemSize={35}
>
{Row}
</FixedSizeList>
);
}
Traditional type-based structure:
src/
components/
hooks/
contexts/
utils/
pages/
Modern feature-based structure:
src/
features/
auth/
components/
hooks/
context/
utils/
users/
components/
hooks/
api.js
shared/
components/
hooks/
A feature-based organization keeps related code together, improving maintainability as applications grow.
Atomic Design breaks interfaces into five levels:
components/
atoms/ # Basic building blocks (Button, Input)
molecules/ # Simple combinations (SearchBar, FormField)
organisms/ # Complex UI sections (Header, ProductCard)
templates/ # Page layouts without specific content
pages/ # Complete pages with real content
This methodology creates consistent interfaces and promotes reusability.
Passing props through multiple components creates maintenance issues.
// Problematic
function App({ user }) {
return <MainContent user={user} />;
}
function MainContent({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserInfo user={user} />;
}
Solution: Use Context or state management for deeply shared data.
Components that do too much become difficult to maintain.
Solution: Break it down into smaller, focused components with single responsibilities.
Creating functions inside render can cause performance issues.
// Problematic
function SearchBar() {
return (
<input
onChange={(e) => {
// New function created on every render
handleChange(e.target.value);
}}
/>
);
}
Solution: Use useCallback or define functions outside render.
Related read: Mastering React Native Apps: useMemo, useCallback, and FlashList
Bypassing React’s declarative approach can cause inconsistencies.
// Anti-pattern
useEffect(() => {
document.getElementById('timer').innerText = `Count: ${count}`;
}, [count]);
Solution: Let React handle the DOM through state and props.
When selecting from available React Design Patterns, ask:
1. Component Organization:
▪️Does this logic need to be shared across components?
▪️Is this component becoming too large?
2. State Management:
▪️Is this state only used by one component?
▪️How many components need to access this state?
3. Performance:
▪️Is this component rendering too often?
▪️Are there expensive calculations that could be memoized?
React Design Patterns play a pivotal role in crafting scalable and maintainable applications. Modern React embraces functional components with Hooks over classes, composition over inheritance, custom Hooks for logic reuse, context for global state, and feature-based organization for large applications.
Remember that no pattern is perfect for all situations. The best approach often involves combining multiple patterns based on your specific needs. As React evolves, stay curious and adapt your approach to incorporate new best practices.
The team at Mindbowser was highly professional, patient, and collaborative throughout our engagement. They struck the right balance between offering guidance and taking direction, which made the development process smooth. Although our project wasn’t related to healthcare, we clearly benefited...
Founder, Texas Ranch Security
Mindbowser played a crucial role in helping us bring everything together into a unified, cohesive product. Their commitment to industry-standard coding practices made an enormous difference, allowing developers to seamlessly transition in and out of the project without any confusion....
CEO, MarketsAI
I'm thrilled to be partnering with Mindbowser on our journey with TravelRite. The collaboration has been exceptional, and I’m truly grateful for the dedication and expertise the team has brought to the development process. Their commitment to our mission is...
Founder & CEO, TravelRite
The Mindbowser team's professionalism consistently impressed me. Their commitment to quality shone through in every aspect of the project. They truly went the extra mile, ensuring they understood our needs perfectly and were always willing to invest the time to...
CTO, New Day Therapeutics
I collaborated with Mindbowser for several years on a complex SaaS platform project. They took over a partially completed project and successfully transformed it into a fully functional and robust platform. Throughout the entire process, the quality of their work...
President, E.B. Carlson
Mindbowser and team are professional, talented and very responsive. They got us through a challenging situation with our IOT product successfully. They will be our go to dev team going forward.
Founder, Cascada
Amazing team to work with. Very responsive and very skilled in both front and backend engineering. Looking forward to our next project together.
Co-Founder, Emerge
The team is great to work with. Very professional, on task, and efficient.
Founder, PeriopMD
I can not express enough how pleased we are with the whole team. From the first call and meeting, they took our vision and ran with it. Communication was easy and everyone was flexible to our schedule. I’m excited to...
Founder, Seeke
We had very close go live timeline and Mindbowser team got us live a month before.
CEO, BuyNow WorldWide
If you want a team of great developers, I recommend them for the next project.
Founder, Teach Reach
Mindbowser built both iOS and Android apps for Mindworks, that have stood the test of time. 5 years later they still function quite beautifully. Their team always met their objectives and I'm very happy with the end result. Thank you!
Founder, Mindworks
Mindbowser has delivered a much better quality product than our previous tech vendors. Our product is stable and passed Well Architected Framework Review from AWS.
CEO, PurpleAnt
I am happy to share that we got USD 10k in cloud credits courtesy of our friends at Mindbowser. Thank you Pravin and Ayush, this means a lot to us.
CTO, Shortlist
Mindbowser is one of the reasons that our app is successful. These guys have been a great team.
Founder & CEO, MangoMirror
Kudos for all your hard work and diligence on the Telehealth platform project. You made it possible.
CEO, ThriveHealth
Mindbowser helped us build an awesome iOS app to bring balance to people’s lives.
CEO, SMILINGMIND
They were a very responsive team! Extremely easy to communicate and work with!
Founder & CEO, TotTech
We’ve had very little-to-no hiccups at all—it’s been a really pleasurable experience.
Co-Founder, TEAM8s
Mindbowser was very helpful with explaining the development process and started quickly on the project.
Executive Director of Product Development, Innovation Lab
The greatest benefit we got from Mindbowser is the expertise. Their team has developed apps in all different industries with all types of social proofs.
Co-Founder, Vesica
Mindbowser is professional, efficient and thorough.
Consultant, XPRIZE
Very committed, they create beautiful apps and are very benevolent. They have brilliant Ideas.
Founder, S.T.A.R.S of Wellness
Mindbowser was great; they listened to us a lot and helped us hone in on the actual idea of the app. They had put together fantastic wireframes for us.
Co-Founder, Flat Earth
Ayush was responsive and paired me with the best team member possible, to complete my complex vision and project. Could not be happier.
Founder, Child Life On Call
The team from Mindbowser stayed on task, asked the right questions, and completed the required tasks in a timely fashion! Strong work team!
CEO, SDOH2Health LLC
Mindbowser was easy to work with and hit the ground running, immediately feeling like part of our team.
CEO, Stealth Startup
Mindbowser was an excellent partner in developing my fitness app. They were patient, attentive, & understood my business needs. The end product exceeded my expectations. Thrilled to share it globally.
Owner, Phalanx
Mindbowser's expertise in tech, process & mobile development made them our choice for our app. The team was dedicated to the process & delivered high-quality features on time. They also gave valuable industry advice. Highly recommend them for app development...
Co-Founder, Fox&Fork