Skip to content

Design Patterns Guide

Design Patterns Guide

Design patterns are typical solutions to common problems in software design. Each pattern is like a blueprint that you can customize to solve a particular design problem in your code. This guide covers both classic design patterns and React-specific patterns that are particularly useful in modern frontend development.

Why Learn Design Patterns?

  • Common Language: Patterns create a shared vocabulary between developers
  • Proven Solutions: Time-tested approaches to common problems
  • Code Quality: Lead to more maintainable and flexible code
  • Problem Recognition: Help identify when and how to apply specific solutions

Classification of Patterns

Design patterns can be categorized into three main groups:

1. Creational Patterns

Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

2. Structural Patterns

Deal with object composition, creating relationships between objects to form larger structures.

3. Behavioral Patterns

Focus on communication between objects and the assignment of responsibilities.



React Anti-Patterns

Understanding what NOT to do is just as important as learning good patterns. Here are common React anti-patterns that can lead to maintainability issues, performance problems, and development headaches.

1. God Component

A component that tries to do everything - handling multiple responsibilities, managing complex state, and containing too much logic.

Problems:

  • Hard to test and debug
  • Difficult to maintain and understand
  • Poor reusability
  • Performance issues
// ❌ God Component Anti-pattern
function UserDashboard() {
const [users, setUsers] = useState([]);
const [products, setProducts] = useState([]);
const [orders, setOrders] = useState([]);
const [analytics, setAnalytics] = useState({});
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
// Hundreds of lines of logic for different concerns...
const fetchUsers = async () => {
/* ... */
};
const fetchProducts = async () => {
/* ... */
};
const generateAnalytics = () => {
/* ... */
};
const handleUserUpdate = () => {
/* ... */
};
const handleProductCreate = () => {
/* ... */
};
// ... many more functions
return (
<div className="dashboard">
{/* Massive JSX handling multiple features */}
<div className="users-section">{/* Complex user management UI */}</div>
<div className="products-section">
{/* Complex product management UI */}
</div>
<div className="analytics-section">{/* Complex analytics UI */}</div>
{/* ... hundreds more lines of JSX */}
</div>
);
}
// ✅ Better approach - Split responsibilities
function UserDashboard() {
return (
<div className="dashboard">
<UserManagement />
<ProductManager />
<OrderHistory />
<AnalyticsPanel />
<NotificationCenter />
</div>
);
}
function UserManagement() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
// Focused logic only for user management
return <div>{/* User-specific UI */}</div>;
}

2. Props Drilling

Passing props through multiple component layers when only the deeply nested component needs them.

Problems:

  • Makes components tightly coupled
  • Hard to refactor
  • Intermediate components receive props they don’t use
  • Maintenance nightmare when prop structure changes
// ❌ Props Drilling Anti-pattern
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const [language, setLanguage] = useState("en");
return (
<Layout
user={user}
setUser={setUser}
theme={theme}
setTheme={setTheme}
language={language}
setLanguage={setLanguage}
/>
);
}
function Layout({ user, setUser, theme, setTheme, language, setLanguage }) {
return (
<div className={`layout ${theme}`}>
<Header
user={user}
setUser={setUser}
theme={theme}
setTheme={setTheme}
language={language}
setLanguage={setLanguage}
/>
<Sidebar user={user} theme={theme} />
</div>
);
}
function Header({ user, setUser, theme, setTheme, language, setLanguage }) {
return (
<header>
<Navigation user={user} />
<UserMenu
user={user}
setUser={setUser}
theme={theme}
setTheme={setTheme}
language={language}
setLanguage={setLanguage}
/>
</header>
);
}
function UserMenu({ user, setUser, theme, setTheme, language, setLanguage }) {
return (
<div className="user-menu">
<span>{user?.name}</span>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
<button onClick={() => setLanguage(language === "en" ? "es" : "en")}>
Change Language
</button>
<button onClick={() => setUser(null)}>Logout</button>
</div>
);
}
// ✅ Better approach - Context API
const AppContext = createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const [language, setLanguage] = useState("en");
return (
<AppContext.Provider
value={{
user,
setUser,
theme,
setTheme,
language,
setLanguage,
}}
>
<Layout />
</AppContext.Provider>
);
}
function Layout() {
const { theme } = useContext(AppContext);
return (
<div className={`layout ${theme}`}>
<Header />
<Sidebar />
</div>
);
}
function UserMenu() {
const { user, setUser, theme, setTheme, language, setLanguage } =
useContext(AppContext);
return (
<div className="user-menu">
<span>{user?.name}</span>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
<button onClick={() => setLanguage(language === "en" ? "es" : "en")}>
Change Language
</button>
<button onClick={() => setUser(null)}>Logout</button>
</div>
);
}

3. Premature Optimization

Optimizing code before identifying actual performance bottlenecks, often making code more complex without real benefits.

Problems:

  • Increased complexity
  • Harder to maintain
  • May not solve real performance issues
  • Time wasted on micro-optimizations
// ❌ Premature Optimization Anti-pattern
function UserList({ users }) {
// Unnecessary useMemo for simple operations
const sortedUsers = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users],
);
// Unnecessary useCallback for simple functions
const handleClick = useCallback((id) => {
console.log("User clicked:", id);
}, []);
// Over-memoization of simple components
const MemoizedUserItem = useMemo(
() =>
memo(({ user, onClick }) => (
<div onClick={() => onClick(user.id)}>
{user.name} - {user.email}
</div>
)),
[],
);
return (
<div>
{sortedUsers.map((user) => (
<MemoizedUserItem key={user.id} user={user} onClick={handleClick} />
))}
</div>
);
}
// ✅ Better approach - Optimize when needed
function UserList({ users }) {
const handleClick = (id) => console.log("User clicked:", id);
return (
<div>
{users
.sort((a, b) => a.name.localeCompare(b.name))
.map((user) => (
<UserItem key={user.id} user={user} onClick={handleClick} />
))}
</div>
);
}
// Only optimize if you have performance issues with many items
function OptimizedUserList({ users }) {
// Use useMemo only for expensive calculations
const sortedUsers = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users],
); // Only when users array actually changes
return (
<VirtualizedList items={sortedUsers}>
{({ item: user }) => <UserItem user={user} />}
</VirtualizedList>
);
}

4. Global State Coupling

Putting everything in global state or making components too dependent on specific global state structure.

Problems:

  • Hard to test components in isolation
  • Tight coupling between components and state structure
  • Difficult to refactor state shape
  • Performance issues with unnecessary re-renders
// ❌ Global State Coupling Anti-pattern
function UserProfile() {
const globalState = useGlobalState();
// Directly accessing deeply nested global state
const userName = globalState.app.user.profile.personal.name;
const userEmail = globalState.app.user.profile.contact.email;
const userAvatar = globalState.app.user.profile.personal.avatar;
const userPreferences = globalState.app.user.settings.preferences;
const notifications = globalState.app.user.notifications.unread;
return (
<div>
<img src={userAvatar} alt={userName} />
<h1>{userName}</h1>
<p>{userEmail}</p>
<div>Notifications: {notifications.length}</div>
<div>Theme: {userPreferences.theme}</div>
</div>
);
}
// ✅ Better approach - Selector pattern and local state
const useUserProfile = () => {
const globalState = useGlobalState();
return useMemo(
() => ({
name: globalState.app.user.profile.personal.name,
email: globalState.app.user.profile.contact.email,
avatar: globalState.app.user.profile.personal.avatar,
}),
[globalState.app.user.profile],
);
};
const useUserPreferences = () => {
const globalState = useGlobalState();
return globalState.app.user.settings.preferences;
};
const useNotifications = () => {
const globalState = useGlobalState();
return globalState.app.user.notifications.unread;
};
function UserProfile() {
const { name, email, avatar } = useUserProfile();
const preferences = useUserPreferences();
const notifications = useNotifications();
return (
<div>
<img src={avatar} alt={name} />
<h1>{name}</h1>
<p>{email}</p>
<div>Notifications: {notifications.length}</div>
<div>Theme: {preferences.theme}</div>
</div>
);
}

5. Components Folder (Poor Organization)

Dumping all components into a single flat folder without proper organization or structure.

Problems:

  • Hard to find specific components
  • No logical grouping
  • Scaling issues as project grows
  • Unclear component relationships
Terminal window
# ❌ Components Folder Anti-pattern
src/
components/
Button.jsx
Header.jsx
Footer.jsx
UserCard.jsx
ProductCard.jsx
OrderItem.jsx
ShippingForm.jsx
PaymentForm.jsx
LoginModal.jsx
SignupModal.jsx
ProductList.jsx
UserList.jsx
OrderHistory.jsx
NavBar.jsx
SideBar.jsx
LoadingSpinner.jsx
ErrorMessage.jsx
SuccessMessage.jsx
FormInput.jsx
FormSelect.jsx
... (50+ more components)
# ✅ Better approach - Organized structure
src/
components/
ui/ # Reusable UI components
Button/
Button.jsx
Button.module.css
Button.stories.js
Input/
Input.jsx
Input.module.css
Modal/
Modal.jsx
Modal.module.css
Spinner/
LoadingSpinner.jsx
layout/ # Layout components
Header/
Header.jsx
Navigation.jsx
Footer/
Footer.jsx
Sidebar/
Sidebar.jsx
features/ # Feature-specific components
auth/
LoginModal.jsx
SignupModal.jsx
AuthForm.jsx
user/
UserCard.jsx
UserList.jsx
UserProfile.jsx
product/
ProductCard.jsx
ProductList.jsx
ProductDetail.jsx
order/
OrderItem.jsx
OrderHistory.jsx
OrderSummary.jsx
forms/ # Form components
ShippingForm.jsx
PaymentForm.jsx
ContactForm.jsx

6. useEffect Driven Development

Using useEffect for everything instead of understanding when it’s actually needed, leading to over-complicated component logic.

Problems:

  • Unnecessary re-renders
  • Complex dependency arrays
  • Hard to debug effect chains
  • Performance issues
  • Race conditions
// ❌ useEffect Driven Development Anti-pattern
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [formattedName, setFormattedName] = useState("");
const [displayAge, setDisplayAge] = useState("");
const [isAdult, setIsAdult] = useState(false);
// Unnecessary effect for simple computation
useEffect(() => {
if (user && user.firstName && user.lastName) {
setFormattedName(`${user.firstName} ${user.lastName}`);
}
}, [user]);
// Another unnecessary effect for simple computation
useEffect(() => {
if (user && user.birthDate) {
const age = calculateAge(user.birthDate);
setDisplayAge(`${age} years old`);
setIsAdult(age >= 18);
}
}, [user]);
// Overly complex effect chain
useEffect(() => {
setLoading(true);
setError(null);
}, [userId]);
useEffect(() => {
if (loading && !error) {
fetchUser(userId)
.then((userData) => {
setUser(userData);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}
}, [loading, error, userId]);
// Yet another effect for analytics
useEffect(() => {
if (user && !loading) {
trackUserView(user.id);
}
}, [user, loading]);
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>Error: {error}</div>}
{user && (
<div>
<h1>{formattedName}</h1>
<p>{displayAge}</p>
{isAdult && <p>Adult User</p>}
</div>
)}
</div>
);
}
// ✅ Better approach - Simpler logic with computed values
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Single effect for data fetching
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetchUser(userId)
.then((userData) => {
if (!cancelled) {
setUser(userData);
trackUserView(userData.id); // Side effect in same place
}
})
.catch((err) => {
if (!cancelled) {
setError(err.message);
}
})
.finally(() => {
if (!cancelled) {
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [userId]);
// Computed values - no effects needed
const formattedName = user ? `${user.firstName} ${user.lastName}` : "";
const age = user ? calculateAge(user.birthDate) : 0;
const displayAge = user ? `${age} years old` : "";
const isAdult = age >= 18;
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return null;
return (
<div>
<h1>{formattedName}</h1>
<p>{displayAge}</p>
{isAdult && <p>Adult User</p>}
</div>
);
}

7. Low Level Code (Reimplementing existing solutions)

Writing complex low-level implementations instead of using existing libraries or React features.

Problems:

  • Reinventing the wheel
  • More bugs and edge cases
  • Maintenance overhead
  • Missing optimizations
  • Security vulnerabilities
// ❌ Low Level Code Anti-pattern
function CustomDatePicker({ value, onChange }) {
const [isOpen, setIsOpen] = useState(false);
const [currentMonth, setCurrentMonth] = useState(new Date().getMonth());
const [currentYear, setCurrentYear] = useState(new Date().getFullYear());
const [selectedDate, setSelectedDate] = useState(value);
// Reimplementing complex date logic
const getDaysInMonth = (month, year) => {
return new Date(year, month + 1, 0).getDate();
};
const getFirstDayOfMonth = (month, year) => {
return new Date(year, month, 1).getDay();
};
const handleDateClick = (day) => {
const newDate = new Date(currentYear, currentMonth, day);
setSelectedDate(newDate);
onChange(newDate);
setIsOpen(false);
};
const renderCalendar = () => {
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
const firstDay = getFirstDayOfMonth(currentMonth, currentYear);
const days = [];
// Complex calendar rendering logic...
for (let i = 0; i < firstDay; i++) {
days.push(<div key={`empty-${i}`} className="empty-day"></div>);
}
for (let day = 1; day <= daysInMonth; day++) {
days.push(
<div
key={day}
className={`day ${selectedDate?.getDate() === day ? "selected" : ""}`}
onClick={() => handleDateClick(day)}
>
{day}
</div>,
);
}
return days;
};
// Manual keyboard navigation, focus management, accessibility...
const handleKeyDown = (e) => {
// Hundreds of lines of keyboard navigation logic
};
return (
<div className="custom-date-picker">
<input
type="text"
value={selectedDate ? selectedDate.toLocaleDateString() : ""}
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
readOnly
/>
{isOpen && (
<div className="calendar-popup">
<div className="calendar-header">
<button onClick={() => setCurrentMonth(currentMonth - 1)}></button>
<span>
{currentMonth + 1}/{currentYear}
</span>
<button onClick={() => setCurrentMonth(currentMonth + 1)}></button>
</div>
<div className="calendar-grid">{renderCalendar()}</div>
</div>
)}
</div>
);
}
// ✅ Better approach - Use established libraries
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
function SimpleDatePicker({ value, onChange }) {
return (
<DatePicker
selected={value}
onChange={onChange}
dateFormat="MM/dd/yyyy"
placeholderText="Select a date"
/>
);
}
// Or with react-hook-form for form integration
import { Controller } from "react-hook-form";
function FormDatePicker({ control, name, rules }) {
return (
<Controller
control={control}
name={name}
rules={rules}
render={({ field }) => (
<DatePicker
selected={field.value}
onChange={field.onChange}
dateFormat="MM/dd/yyyy"
/>
)}
/>
);
}

8. Heavy Work (Expensive calculations on every render)

Running expensive calculations on every render instead of memoizing them, causing unnecessary performance bottlenecks.

Problems:

  • Expensive calculations run on every render
  • Performance degradation with complex computations
  • Unresponsive UI during heavy calculations
  • Unnecessary CPU usage
// ❌ Heavy Work Anti-pattern
function DataDashboard({ data, filters, sortOrder }) {
const [selectedItems, setSelectedItems] = useState([]);
const [viewMode, setViewMode] = useState("grid");
// Expensive calculation runs on EVERY render
const processedData = data
.filter((item) => {
// Complex filtering logic
return (
filters.categories.includes(item.category) &&
item.price >= filters.minPrice &&
item.price <= filters.maxPrice &&
item.name.toLowerCase().includes(filters.search.toLowerCase())
);
})
.map((item) => {
// Heavy transformation logic
return {
...item,
score: calculateComplexScore(item), // Expensive function
recommendations: generateRecommendations(item), // Another expensive function
analytics: computeAnalytics(item.history), // Very expensive
};
})
.sort((a, b) => {
// Complex sorting logic
switch (sortOrder) {
case "score":
return b.score - a.score;
case "price":
return a.price - b.price;
case "popularity":
return calculatePopularity(b) - calculatePopularity(a); // Expensive
default:
return a.name.localeCompare(b.name);
}
});
// This expensive calculation runs even when only selectedItems or viewMode changes!
const totalValue = processedData.reduce((sum, item) => {
return sum + item.price * item.quantity * item.score; // Complex calculation
}, 0);
return (
<div>
<div>Total Value: ${totalValue.toFixed(2)}</div>
<div>
View Mode:
<button
onClick={() => setViewMode(viewMode === "grid" ? "list" : "grid")}
>
Toggle View
</button>
</div>
<div>
{processedData.map((item) => (
<ItemCard
key={item.id}
item={item}
selected={selectedItems.includes(item.id)}
onSelect={() => {
setSelectedItems((prev) =>
prev.includes(item.id)
? prev.filter((id) => id !== item.id)
: [...prev, item.id],
);
}}
/>
))}
</div>
</div>
);
}
// ✅ Better approach - Use useMemo for expensive calculations
function DataDashboard({ data, filters, sortOrder }) {
const [selectedItems, setSelectedItems] = useState([]);
const [viewMode, setViewMode] = useState("grid");
// Memoize expensive data processing - only recalculates when dependencies change
const processedData = useMemo(() => {
console.log("Processing data..."); // This should only log when data/filters/sortOrder change
return data
.filter((item) => {
return (
filters.categories.includes(item.category) &&
item.price >= filters.minPrice &&
item.price <= filters.maxPrice &&
item.name.toLowerCase().includes(filters.search.toLowerCase())
);
})
.map((item) => ({
...item,
score: calculateComplexScore(item),
recommendations: generateRecommendations(item),
analytics: computeAnalytics(item.history),
}))
.sort((a, b) => {
switch (sortOrder) {
case "score":
return b.score - a.score;
case "price":
return a.price - b.price;
case "popularity":
return calculatePopularity(b) - calculatePopularity(a);
default:
return a.name.localeCompare(b.name);
}
});
}, [data, filters, sortOrder]); // Only recalculates when these change
// Memoize total value calculation
const totalValue = useMemo(() => {
return processedData.reduce((sum, item) => {
return sum + item.price * item.quantity * item.score;
}, 0);
}, [processedData]); // Only recalculates when processedData changes
// Memoize the selection handler to prevent unnecessary re-renders
const handleItemSelect = useCallback((itemId) => {
setSelectedItems((prev) =>
prev.includes(itemId)
? prev.filter((id) => id !== itemId)
: [...prev, itemId],
);
}, []);
return (
<div>
<div>Total Value: ${totalValue.toFixed(2)}</div>
<div>
View Mode:
<button
onClick={() => setViewMode(viewMode === "grid" ? "list" : "grid")}
>
Toggle View
</button>
</div>
<div className={viewMode}>
{processedData.map((item) => (
<ItemCard
key={item.id}
item={item}
selected={selectedItems.includes(item.id)}
onSelect={handleItemSelect}
/>
))}
</div>
</div>
);
}
// Even better: Custom hook for data processing
function useProcessedData(data, filters, sortOrder) {
return useMemo(() => {
return data
.filter((item) => {
return (
filters.categories.includes(item.category) &&
item.price >= filters.minPrice &&
item.price <= filters.maxPrice &&
item.name.toLowerCase().includes(filters.search.toLowerCase())
);
})
.map((item) => ({
...item,
score: calculateComplexScore(item),
recommendations: generateRecommendations(item),
analytics: computeAnalytics(item.history),
}))
.sort((a, b) => {
switch (sortOrder) {
case "score":
return b.score - a.score;
case "price":
return a.price - b.price;
case "popularity":
return calculatePopularity(b) - calculatePopularity(a);
default:
return a.name.localeCompare(b.name);
}
});
}, [data, filters, sortOrder]);
}

9. Props Plowing (Passing excessive or irrelevant props)

Passing too many props to components or passing props that components don’t actually need, creating bloated component interfaces.

Problems:

  • Components become harder to understand and maintain
  • Unclear component responsibilities
  • Performance issues with unnecessary re-renders
  • Makes testing more complex
  • Tight coupling between components
  • Spreading props directly onto DOM elements creates invalid HTML attributes and React warnings
// ❌ Props Plowing Anti-pattern
function UserCard({
user,
isSelected,
onSelect,
theme,
language,
currency,
timezone,
permissions,
analytics,
deviceInfo,
appVersion,
debugMode,
experimentFlags,
onUserClick,
onUserHover,
onUserFocus,
onAnalyticsEvent,
formatDate,
formatCurrency,
translateText,
validatePermission,
logEvent,
trackClick,
showTooltip,
hideTooltip,
openModal,
closeModal,
redirectTo,
refreshData,
cacheData,
}) {
// Component only uses a few of these props
const handleClick = () => {
onSelect(user.id);
trackClick("user_card", user.id);
logEvent("user_interaction", { userId: user.id });
};
return (
<div
className={`user-card ${theme} ${isSelected ? "selected" : ""}`}
onClick={handleClick}
>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
{debugMode && <span>Debug: {user.id}</span>}
</div>
);
}
// ❌ Another Props Plowing example - Kitchen sink component
function ProductDisplay({
product,
user,
cart,
wishlist,
reviews,
recommendations,
categories,
brands,
suppliers,
inventory,
pricing,
discounts,
shipping,
returns,
warranty,
specifications,
images,
videos,
documents,
analytics,
experiments,
ab_tests,
feature_flags,
permissions,
settings,
preferences,
locale,
currency,
timezone,
device,
browser,
session,
history,
breadcrumbs,
metadata,
tracking,
monitoring,
logging,
caching,
api_client,
error_handler,
notification_service,
modal_service,
tooltip_service,
loading_service,
validation_service,
formatting_service,
translation_service,
theme_service,
layout_service,
navigation_service,
search_service,
filter_service,
sort_service,
pagination_service,
export_service,
import_service,
backup_service,
sync_service,
offline_service,
push_notification_service,
email_service,
sms_service,
social_media_service,
payment_service,
subscription_service,
license_service,
security_service,
audit_service,
compliance_service,
gdpr_service,
accessibility_service,
performance_service,
seo_service,
// ... and many more
}) {
// This component is impossible to understand, test, or maintain
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Uses only a tiny fraction of the passed props */}
</div>
);
}
// ❌ Passing all app state as props
function Header({
user,
cart,
notifications,
settings,
theme,
language,
currency,
location,
history,
search,
filters,
products,
categories,
orders,
wishlist,
reviews,
recommendations,
analytics,
experiments,
permissions,
// ... entire application state
onLogin,
onLogout,
onSearch,
onFilter,
onSort,
onNavigate,
onThemeChange,
onLanguageChange,
onCurrencyChange,
// ... all possible actions
}) {
// Header only needs user, cart count, and a few actions
return (
<header>
<Logo />
<SearchBar onSearch={onSearch} />
<CartIcon count={cart.items.length} />
<UserMenu user={user} onLogout={onLogout} />
</header>
);
}
// ❌ Spreading props directly onto DOM elements
function BadButton({
onClick,
disabled,
children,
analytics,
user,
theme,
...props
}) {
// Dangerous: spreading all props onto DOM element
// This can add invalid HTML attributes and cause React warnings
return (
<button
onClick={onClick}
disabled={disabled}
{...props} // ❌ analytics, user, theme become invalid HTML attributes
>
{children}
</button>
);
}
// ❌ Another example of bad prop spreading
function BadInput({
value,
onChange,
validation,
formatting,
analytics,
tracking,
...allProps
}) {
return (
<input
value={value}
onChange={onChange}
{...allProps} // ❌ validation, formatting, analytics, tracking are not valid HTML attributes
/>
);
}
// ✅ Better approach - Focused component interfaces
function UserCard({ user, isSelected, onSelect, theme }) {
const { trackClick } = useAnalytics();
const { logEvent } = useLogger();
const handleClick = () => {
onSelect(user.id);
trackClick('user_card', user.id);
logEvent('user_interaction', { userId: user.id });
};
return (
<div
className={`user-card ${theme} ${isSelected ? 'selected' : ''}`}
onClick={handleClick}
>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// ✅ Focused product component with specific props
function ProductDisplay({ product }) {
const { user } = useAuth();
const { addToCart } = useCart();
const { addToWishlist } = useWishlist();
const { currency } = useLocalization();
const { trackEvent } = useAnalytics();
const handleAddToCart = () => {
addToCart(product);
trackEvent('add_to_cart', { productId: product.id });
};
return (
<div className="product-display">
<ProductImages images={product.images} />
<ProductInfo
name={product.name}
description={product.description}
price={product.price}
currency={currency}
/>
<ProductActions
onAddToCart={handleAddToCart}
onAddToWishlist={() => addToWishlist(product)}
inStock={product.inventory > 0}
/>
{user && <ProductReviews productId={product.id} />}
</div>
);
}
// ✅ Clean header with minimal props
function Header() {
const { user, logout } = useAuth();
const { cartCount } = useCart();
const { search, setSearch } = useSearch();
return (
<header>
<Logo />
<SearchBar value={search} onChange={setSearch} />
<CartIcon count={cartCount} />
<UserMenu user={user} onLogout={logout} />
</header>
);
}
// ✅ Component composition approach
function UserProfile({ userId }) {
return (
<div className="user-profile">
<UserAvatar userId={userId} />
<UserInfo userId={userId} />
<UserStats userId={userId} />
<UserActions userId={userId} />
</div>
);
}
function UserAvatar({ userId }) {
const { user } = useUser(userId);
const { theme } = useTheme();
return (
<div className={`user-avatar ${theme}`}>
<img src={user.avatar} alt={user.name} />
</div>
);
}
function UserInfo({ userId }) {
const { user } = useUser(userId);
const { formatDate } = useFormatting();
return (
<div className="user-info">
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Member since: {formatDate(user.createdAt)}</p>
</div>
);
}
// ✅ Props interface definition (TypeScript)
interface UserCardProps {
user: {
id: string;
name: string;
email: string;
avatar: string;
};
isSelected: boolean;
onSelect: (userId: string) => void;
variant?: 'default' | 'compact' | 'detailed';
}
function TypedUserCard({ user, isSelected, onSelect, variant = 'default' }: UserCardProps) {
// Clear interface with only necessary props
return (
<div className={`user-card user-card--${variant}`}>
{/* Component implementation */}
</div>
);
}
// ✅ Using render props to avoid prop plowing
function DataProvider({ children, userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser).catch(setError).finally(() => setLoading(false));
}, [userId]);
return children({ user, loading, error, refetch: () => fetchUser(userId) });
}
function UserProfileWithRenderProps({ userId }) {
return (
<DataProvider userId={userId}>
{({ user, loading, error, refetch }) => {
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
if (!user) return <UserNotFound />;
return (
<div>
<UserCard user={user} />
<UserStats user={user} />
<UserActions user={user} />
</div>
);
}}
</DataProvider>
);
}
// ✅ Proper way to handle DOM props spreading
function GoodButton({
onClick,
disabled,
children,
variant = 'primary',
size = 'medium',
analytics,
user,
theme,
...domProps // Only spread valid DOM attributes
}) {
const { trackClick } = useAnalytics();
const { theme: currentTheme } = useTheme();
// Filter out only valid DOM attributes
const validDomProps = {};
const validAttributes = ['className', 'style', 'id', 'data-testid', 'aria-label', 'title'];
Object.keys(domProps).forEach(key => {
if (validAttributes.includes(key) || key.startsWith('data-') || key.startsWith('aria-')) {
validDomProps[key] = domProps[key];
}
});
const handleClick = (e) => {
onClick?.(e);
trackClick('button_click', { variant, user: user?.id });
};
return (
<button
onClick={handleClick}
disabled={disabled}
className={`btn btn--${variant} btn--${size} btn--${currentTheme}`}
{...validDomProps} // ✅ Only valid DOM attributes
>
{children}
</button>
);
}
// ✅ Alternative: Explicit prop extraction
function GoodInput({
value,
onChange,
validation,
formatting,
analytics,
tracking,
// Explicitly destructure valid DOM props
className,
placeholder,
disabled,
readOnly,
'data-testid': testId,
'aria-label': ariaLabel,
...restProps
}) {
const { validateField } = useValidation();
const { formatValue } = useFormatting();
const { trackEvent } = useAnalytics();
const handleChange = (e) => {
const formatted = formatValue(e.target.value, formatting);
const isValid = validateField(formatted, validation);
onChange?.(e, { value: formatted, isValid });
trackEvent('input_change', { field: testId });
};
// Only pass valid HTML attributes to DOM element
return (
<input
value={value}
onChange={handleChange}
className={className}
placeholder={placeholder}
disabled={disabled}
readOnly={readOnly}
data-testid={testId}
aria-label={ariaLabel}
// Don't spread restProps - validate them first if needed
/>
);
}
// ✅ Using a whitelist approach
const VALID_DIV_PROPS = [
'className', 'style', 'id', 'onClick', 'onMouseEnter', 'onMouseLeave',
'role', 'tabIndex', 'title'
];
function SafeContainer({ children, analytics, theme, ...props }) {
const { trackEvent } = useAnalytics();
// Filter props to only include valid DOM attributes
const domProps = Object.keys(props)
.filter(key => VALID_DIV_PROPS.includes(key) || key.startsWith('data-') || key.startsWith('aria-'))
.reduce((obj, key) => {
obj[key] = props[key];
return obj;
}, {});
useEffect(() => {
trackEvent('container_render', { theme });
}, [trackEvent, theme]);
return (
<div {...domProps}>
{children}
</div>
);
}

10. Not Using useCallback When It Would Be Beneficial

Functions declared inside functional components are re-created on every render, which can cause unnecessary re-renders of child components when these functions are passed as props.

Problems:

  • Unnecessary re-renders of child components
  • Performance degradation in complex component trees
  • Breaking React.memo optimizations
  • Inefficient re-creation of stable callback functions
// ❌ Not using useCallback when beneficial
function ParentWithoutCallback() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// ❌ This function is recreated on every render
const handleClick = () => {
console.log("Button clicked");
// Some expensive operation
performExpensiveOperation();
};
// ❌ Event handlers recreated on every render
const handleSubmit = (formData) => {
console.log("Form submitted:", formData);
submitToAPI(formData);
};
const handleReset = () => {
setName("");
setCount(0);
};
return (
<div>
<h1>Count: {count}</h1>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
{/* These child components will re-render unnecessarily
every time parent re-renders due to name state change */}
<ExpensiveButton onClick={handleClick} />
<ExpensiveForm onSubmit={handleSubmit} />
<ResetButton onClick={handleReset} />
<CounterDisplay
count={count}
onIncrement={() => setCount((c) => c + 1)}
/>
</div>
);
}
// ❌ Child component without proper memoization
function ExpensiveButton({ onClick }) {
console.log("ExpensiveButton rendered"); // This will log on every parent re-render
return (
<button onClick={onClick} className="expensive-btn">
{/* Imagine expensive rendering logic here */}
{Array.from({ length: 1000 }).map((_, i) => (
<span key={i} className="expensive-span">
</span>
))}
Click me
</button>
);
}
// ❌ Form component that re-renders unnecessarily
function ExpensiveForm({ onSubmit }) {
const [formData, setFormData] = useState({ email: "", message: "" });
console.log("ExpensiveForm rendered"); // Logs on every parent re-render
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.email}
onChange={(e) =>
setFormData((prev) => ({ ...prev, email: e.target.value }))
}
placeholder="Email"
/>
<textarea
value={formData.message}
onChange={(e) =>
setFormData((prev) => ({ ...prev, message: e.target.value }))
}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
}
// ❌ List component with recreated item handlers
function TodoList({ todos, updateTodo, deleteTodo }) {
const [filter, setFilter] = useState("all");
const filteredTodos = todos.filter((todo) => {
if (filter === "completed") return todo.completed;
if (filter === "active") return !todo.completed;
return true;
});
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
// ❌ These functions are recreated on every render
onUpdate={(id, updates) => updateTodo(id, updates)}
onDelete={(id) => deleteTodo(id)}
/>
))}
</div>
);
}
// ✅ Using useCallback when beneficial
function ParentWithCallback() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// ✅ Memoized callback - only recreated if dependencies change
const handleClick = useCallback(() => {
console.log("Button clicked");
performExpensiveOperation();
}, []); // No dependencies, so created only once
// ✅ Memoized with stable reference
const handleSubmit = useCallback((formData) => {
console.log("Form submitted:", formData);
submitToAPI(formData);
}, []); // No dependencies needed
// ✅ Memoized callback that depends on setter functions (which are stable)
const handleReset = useCallback(() => {
setName("");
setCount(0);
}, []); // setName and setCount are stable, so no deps needed
// ✅ Callback with dependency
const handleNamedIncrement = useCallback(() => {
console.log(`Incrementing for ${name}`);
setCount((c) => c + 1);
}, [name]); // Recreated only when name changes
return (
<div>
<h1>Count: {count}</h1>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
{/* These child components won't re-render unnecessarily */}
<ExpensiveButton onClick={handleClick} />
<ExpensiveForm onSubmit={handleSubmit} />
<ResetButton onClick={handleReset} />
<CounterDisplay
count={count}
onIncrement={useCallback(() => setCount((c) => c + 1), [])}
/>
</div>
);
}
// ✅ Properly memoized child component
const ExpensiveButton = React.memo(({ onClick }) => {
console.log("ExpensiveButton rendered"); // Only logs when onClick reference changes
return (
<button onClick={onClick} className="expensive-btn">
{Array.from({ length: 1000 }).map((_, i) => (
<span key={i} className="expensive-span">
</span>
))}
Click me
</button>
);
});
// ✅ Memoized form component
const ExpensiveForm = React.memo(({ onSubmit }) => {
const [formData, setFormData] = useState({ email: "", message: "" });
console.log("ExpensiveForm rendered"); // Only when onSubmit changes
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
onSubmit(formData);
},
[formData, onSubmit],
);
const handleEmailChange = useCallback((e) => {
setFormData((prev) => ({ ...prev, email: e.target.value }));
}, []);
const handleMessageChange = useCallback((e) => {
setFormData((prev) => ({ ...prev, message: e.target.value }));
}, []);
return (
<form onSubmit={handleSubmit}>
<input
value={formData.email}
onChange={handleEmailChange}
placeholder="Email"
/>
<textarea
value={formData.message}
onChange={handleMessageChange}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
});
// ✅ Optimized list with memoized callbacks
function TodoList({ todos, updateTodo, deleteTodo }) {
const [filter, setFilter] = useState("all");
// ✅ Memoized filter function
const filteredTodos = useMemo(() => {
return todos.filter((todo) => {
if (filter === "completed") return todo.completed;
if (filter === "active") return !todo.completed;
return true;
});
}, [todos, filter]);
// ✅ Memoized handlers
const handleUpdate = useCallback(
(id, updates) => {
updateTodo(id, updates);
},
[updateTodo],
);
const handleDelete = useCallback(
(id) => {
deleteTodo(id);
},
[deleteTodo],
);
const handleFilterChange = useCallback((e) => {
setFilter(e.target.value);
}, []);
return (
<div>
<select value={filter} onChange={handleFilterChange}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
))}
</div>
);
}
// ✅ Custom hook for stable callbacks
function useStableCallback(callback, deps) {
const callbackRef = useRef(callback);
const depsRef = useRef(deps);
// Update callback if dependencies changed
if (!depsRef.current || !deps.every((dep, i) => dep === depsRef.current[i])) {
callbackRef.current = callback;
depsRef.current = deps;
}
return useCallback((...args) => callbackRef.current(...args), []);
}
// ✅ Using the custom hook
function ComponentWithStableCallback({ data, onDataChange }) {
const stableCallback = useStableCallback(
(newData) => {
console.log("Processing data:", newData);
onDataChange(newData);
},
[onDataChange],
);
return <ChildComponent onUpdate={stableCallback} />;
}
// ✅ When NOT to use useCallback (avoid premature optimization)
function SimpleComponent() {
const [count, setCount] = useState(0);
// ❌ Don't do this - premature optimization
// const handleClick = useCallback(() => setCount(c => c + 1), []);
// ✅ This is fine - simple handler without child components
const handleClick = () => setCount((c) => c + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
{/* No child components receiving this callback */}
</div>
);
}

11. Messy Events (Inline arrow functions with parameters)

Using inline arrow functions for event handlers that need additional parameters, creating messy JSX and potential performance issues.

Problems:

  • Creates new function instances on every render
  • Makes JSX hard to read and maintain
  • Can cause unnecessary re-renders of child components
  • Difficult to test event handlers
// ❌ Messy Events Anti-pattern
function ProductList({ products, onProductUpdate, onProductDelete }) {
const [selectedCategory, setSelectedCategory] = useState("all");
const [sortOrder, setSortOrder] = useState("asc");
const handleQuantityChange = (productId, newQuantity, event) => {
event.preventDefault();
onProductUpdate(productId, { quantity: newQuantity });
};
const handlePriceChange = (productId, newPrice, currency, event) => {
event.preventDefault();
onProductUpdate(productId, { price: newPrice, currency });
};
const confirmDelete = (productId, productName, event) => {
event.preventDefault();
if (window.confirm(`Delete ${productName}?`)) {
onProductDelete(productId);
}
};
return (
<div>
{products.map((product) => (
<div key={product.id} className="product-item">
<h3>{product.name}</h3>
{/* Messy inline arrow functions */}
<input
type="number"
value={product.quantity}
onChange={(e) =>
handleQuantityChange(product.id, e.target.value, e)
}
/>
<input
type="number"
value={product.price}
onChange={(e) =>
handlePriceChange(product.id, e.target.value, product.currency, e)
}
/>
<select
value={product.category}
onChange={(e) =>
onProductUpdate(product.id, { category: e.target.value })
}
>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
</select>
<button onClick={(e) => confirmDelete(product.id, product.name, e)}>
Delete
</button>
{/* Even messier with multiple parameters */}
<button
onClick={(e) => {
e.preventDefault();
onProductUpdate(product.id, {
featured: !product.featured,
lastModified: new Date().toISOString(),
});
}}
>
{product.featured ? "Unfeature" : "Feature"}
</button>
</div>
))}
</div>
);
}
// ✅ Better approach - Curried functions and clean event handlers
function ProductList({ products, onProductUpdate, onProductDelete }) {
const [selectedCategory, setSelectedCategory] = useState("all");
const [sortOrder, setSortOrder] = useState("asc");
// Curried function for quantity changes
const handleQuantityChange = (productId) => (event) => {
event.preventDefault();
onProductUpdate(productId, { quantity: event.target.value });
};
// Curried function for price changes
const handlePriceChange = (productId, currency) => (event) => {
event.preventDefault();
onProductUpdate(productId, {
price: event.target.value,
currency,
});
};
// Curried function for category changes
const handleCategoryChange = (productId) => (event) => {
onProductUpdate(productId, { category: event.target.value });
};
// Curried function for delete confirmation
const handleDelete = (productId, productName) => (event) => {
event.preventDefault();
if (window.confirm(`Delete ${productName}?`)) {
onProductDelete(productId);
}
};
// Curried function for feature toggle
const handleFeatureToggle = (productId, currentFeatured) => (event) => {
event.preventDefault();
onProductUpdate(productId, {
featured: !currentFeatured,
lastModified: new Date().toISOString(),
});
};
return (
<div>
{products.map((product) => (
<div key={product.id} className="product-item">
<h3>{product.name}</h3>
{/* Clean, readable event handlers */}
<input
type="number"
value={product.quantity}
onChange={handleQuantityChange(product.id)}
/>
<input
type="number"
value={product.price}
onChange={handlePriceChange(product.id, product.currency)}
/>
<select
value={product.category}
onChange={handleCategoryChange(product.id)}
>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
</select>
<button onClick={handleDelete(product.id, product.name)}>
Delete
</button>
<button onClick={handleFeatureToggle(product.id, product.featured)}>
{product.featured ? "Unfeature" : "Feature"}
</button>
</div>
))}
</div>
);
}
// Alternative approach: Using data attributes
function ProductListWithDataAttributes({
products,
onProductUpdate,
onProductDelete,
}) {
// Single event handler that reads data attributes
const handleInputChange = (event) => {
const { value, dataset, type } = event.target;
const { productId, field, currency } = dataset;
const updateData = { [field]: type === "number" ? Number(value) : value };
if (currency) updateData.currency = currency;
onProductUpdate(productId, updateData);
};
const handleButtonClick = (event) => {
const { dataset } = event.target;
const { action, productId, productName, currentFeatured } = dataset;
switch (action) {
case "delete":
if (window.confirm(`Delete ${productName}?`)) {
onProductDelete(productId);
}
break;
case "toggle-feature":
onProductUpdate(productId, {
featured: currentFeatured !== "true",
lastModified: new Date().toISOString(),
});
break;
}
};
return (
<div>
{products.map((product) => (
<div key={product.id} className="product-item">
<h3>{product.name}</h3>
<input
type="number"
value={product.quantity}
data-product-id={product.id}
data-field="quantity"
onChange={handleInputChange}
/>
<input
type="number"
value={product.price}
data-product-id={product.id}
data-field="price"
data-currency={product.currency}
onChange={handleInputChange}
/>
<select
value={product.category}
data-product-id={product.id}
data-field="category"
onChange={handleInputChange}
>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
</select>
<button
data-action="delete"
data-product-id={product.id}
data-product-name={product.name}
onClick={handleButtonClick}
>
Delete
</button>
<button
data-action="toggle-feature"
data-product-id={product.id}
data-current-featured={product.featured}
onClick={handleButtonClick}
>
{product.featured ? "Unfeature" : "Feature"}
</button>
</div>
))}
</div>
);
}

How to Avoid React Anti-Patterns

  1. Follow Single Responsibility Principle - Each component should have one clear purpose
  2. Use appropriate state management - Local state for component-specific data, Context/Redux for shared data
  3. Measure before optimizing - Use React DevTools Profiler to identify real performance bottlenecks
  4. Organize code logically - Group related components and use consistent naming conventions
  5. Understand React’s built-in optimizations - Don’t over-engineer simple solutions
  6. Leverage existing libraries - Don’t reinvent common UI patterns (date pickers, forms, etc.)
  7. Keep effects simple and focused - Use useEffect only for side effects, not computations
  8. Use proper TypeScript - Type safety helps catch many anti-patterns early
  9. Write tests - Tests often reveal when components are doing too much
  10. Regular code reviews - Fresh eyes can spot anti-patterns you might miss

Best Practices

When to Use Patterns

  1. Identify the Problem First: Don’t use patterns for the sake of using them
  2. Consider Alternatives: Sometimes a simple solution is better than a pattern
  3. Team Knowledge: Ensure your team understands the patterns you’re using
  4. Performance Impact: Some patterns can impact performance, especially in React

Pattern Categories

This document covers several categories of design patterns:

  • 📋 Gang of Four (GoF) Patterns: Classic design patterns adapted for modern React development
  • ⚛️ React-Specific Patterns: Patterns unique to React ecosystem and component architecture
  • ❌ Anti-Patterns: Common mistakes and problematic approaches to avoid in React applications

Each pattern includes detailed explanations, code examples, and practical use cases. The patterns are described in detail in the sections below.

Resources

Conclusion

Design patterns are powerful tools that can help you write better, more maintainable code. However, remember that patterns are solutions to specific problems. Understanding the problem you’re trying to solve is more important than knowing all the patterns by heart.

Start with simple solutions and refactor to patterns when complexity grows. Focus on the patterns that are most relevant to your current projects and gradually expand your knowledge as needed.