The truth is, components are deceptively simple. It’s easy to get started—define a function, return some JSX, and call it a day. But writing components that are clean, maintainable, and pleasant to work with? That’s a whole different ballgame.
Without realizing it, we create components that are:
- Too big to understand at a glance.
- Painfully difficult to test.
- So tightly coupled they’re impossible to reuse.
- Sluggish because of poor performance decisions.
In this post, I’ll walk you through the most common mistakes developers make with React components. More importantly, I’ll show you how to fix them without tearing apart your entire app.
Whether you’re just getting started or have years of experience, these tips will help you write components that are not just functional, but a joy to maintain.
The "Everything Component" Anti-pattern
Let’s talk about the classic blunder we’ve all made: the everything component. You’ve seen it—it starts small and innocent, maybe as a simple form or dashboard. Fast forward a bit, and now it’s managing state, handling API calls, formatting data, and possibly brewing your morning coffee.
// Please, no more of this
const UserDashboard = () => {
const [userData, setUserData] = useState(null);
const [orders, setOrders] = useState([]);
const [notifications, setNotifications] = useState([]);
const [settings, setSettings] = useState({});
const [isEditing, setIsEditing] = useState(false);
const [activeTab, setActiveTab] = useState('profile');
// 15 separate useEffects
// 10 different event handlers
// Multiple conditional renders
// 300+ lines of chaos
};
Sound familiar?
How to Tell If You’re Guilty
Your component might have turned into an "everything component" if:
- State overload: You’re tracking more than 3-4 separate pieces of state.
- Endless scrolling: You spend too much time hunting for specific functions or logic.
- Dependency bloat: Your useEffect dependencies look like your weekly shopping list.
- Feature creep denial: You keep telling yourself, just one more feature won’t hurt.
Breaking It Down
The solution? Instead of a single everything component, split responsibilities into smaller, focused pieces.
// A cleaner, smarter approach
const UserDashboard = () => {
return (
<div>
<UserProfile />
<OrderHistory />
<NotificationCenter />
<UserSettings />
</div>
);
};
Key Principle: Logic > Layout
When refactoring, don’t break components based on how they look on the screen. Split them by responsibility. Ask yourself: Does this functionality deserve its own component? If it’s handling something distinct—like user profiles or order history—it probably does.
Tip: A good component does one thing and does it well. If you struggle to describe its purpose in a single sentence, chances are it’s trying to do too much.
Prop Drilling Hell
Let’s discuss the not-so-fun game of “pass the props”. If you’ve ever passed the same prop through multiple components just to reach a deeply nested child, you’ve been stuck in prop drilling hell.
// This is exhausting
const App = () => {
const [user, setUser] = useState({});
return (
<Layout user={user}>
<Sidebar user={user}>
<Navigation user={user}>
<UserMenu user={user} />
</Navigation>
</Sidebar>
</Layout>
);
};
This approach isn’t just annoying—it creates long-term problems. Imagine needing to rename the user prop. Suddenly, you’re updating it in five or more places. Worse, you end up tying components to data they don’t even use.
How to Fix This
There’s no need to play hot potato with your props. Here are two practical solutions to avoid drilling altogether.
1. Use Context for Shared Data
If a piece of data is accessed across different parts of your app, React’s Context API can simplify things.
const UserContext = createContext();
const App = () => {
const [user, setUser] = useState({});
return (
<UserContext.Provider value={user}>
<Layout>
<Sidebar>
<Navigation />
</Sidebar>
</Layout>
</UserContext.Provider>
);
};
// Use it only where needed
const UserMenu = () => {
const user = useContext(UserContext);
return <div>{user.name}</div>;
};
2. Use Composition for Flexibility
Instead of forcing props through layers, restructure components so they only pass down what’s needed.
// Focused components for better clarity
const Navigation = ({ children }) => {
return <nav>{children}</nav>;
};
// Pass data only where required
const App = () => {
const user = useUser();
return (
<Layout>
<Navigation>
<UserMenu user={user} />
</Navigation>
</Layout>
);
};
Key Takeaways
Context works well for app-wide data like user information, themes, or global settings. However, it’s not always the best option—don’t overuse it. For localized state, think about whether your component structure can be adjusted to avoid drilling entirely.
The goal is to make your components clear and maintainable. Avoiding prop drilling will save you time, frustration, and countless headaches down the road.
Premature Optimization Traps
You've probably heard that famous quote about premature optimization being the root of all evil. Well, welcome to component-level evil. I'm talking about those times we try to make everything reusable before we even know if we'll need it twice.
Here's what it usually looks like:
// Behold, the over-engineered button
const Button = ({
children,
variant = 'primary',
size = 'medium',
isFullWidth = false,
isDisabled = false,
isLoading = false,
leftIcon,
rightIcon,
onClick,
customClassName,
style,
loadingText = 'Loading...',
tooltipText,
animationType,
// ... 10 more props
}) => {
// 50 lines of prop processing logic
return (
<button
className={generateComplexClassNames()}
style={processStyles()}
// ... more complexity
>
{renderButtonContent()}
</button>
);
}
Why This Hurts
- Your “simple” button now requires an instruction manual.
- Most of those 15+ props will never be used.
- Making updates becomes risky because you have to account for endless combinations.
- Writing tests becomes painful, with a hundred possible scenarios to consider.
Better Approach:
Instead of building for every imaginable scenario, start small and let your components grow as needed.
// Start simple
const Button = ({ children, onClick, variant = 'primary' }) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
// Create specific buttons when you actually need them
const LoadingButton = ({ isLoading, children, ...props }) => {
return (
<Button {...props}>
{isLoading ? 'Loading...' : children}
</Button>
);
}
Let your components evolve naturally. Build for what you know you need today. If new requirements arise, add features when you can clearly justify them. Premature optimization wastes time, adds complexity, and rarely pays off.
Remember: The YAGNI principle (You Aren’t Gonna Need It) applies to components too. The best abstractions come when you’ve actually encountered the problems they’re solving. Over-engineering might feel proactive, but simplicity wins every time.
Side Effect Mismanagement
Here’s a classic example of bad effect management. Look familiar?
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// Dependency array woes
useEffect(() => {
fetchUserData(userId);
fetchUserPosts(userId);
// No cleanup? Yikes.
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Multiple effects, all tangled
useEffect(() => {
const subscription = subscribeToUserStatus(userId);
}, [userId]);
// Cleanup? What cleanup?
useEffect(() => {
const interval = setInterval(checkNotifications, 5000);
}, []);
};
Common Mistakes and Fixes
1) Messy Data Fetching
Bad data handling creates more bugs than it solves. Here's a cleaner approach:
// Improved version
const UserProfile = ({ userId }) => {
const { data: user, isLoading } = useQuery(
['user', userId],
() => fetchUserData(userId)
);
// Keep concerns separate
const { data: posts } = useQuery(
['posts', userId],
() => fetchUserPosts(userId),
{ enabled: !!user }
);
};
2) Forgetting Cleanup
Always clean up after yourself:
useEffect(() => {
const subscription = subscribeToUserStatus(userId);
return () => subscription.unsubscribe(); // Cleanup is non-negotiable
}, [userId]);
3) Ignoring Race Conditions
Avoid overlapping requests with this technique:
useEffect(() => {
let isCurrentRequest = true;
async function loadData() {
const data = await fetchUserData(userId);
if (isCurrentRequest) {
setUser(data);
}
}
loadData();
return () => {
isCurrentRequest = false;
};
}, [userId]);
Quick Tips
- Think before using useEffect: Sometimes, you might not need it at all.
- Keep it focused: One effect should handle one responsibility.
- Always clean up: Subscriptions, intervals, and event listeners need attention.
- Use the right tools: Libraries like React Query simplify data fetching and caching.
- Don't cheat with eslint-disable: Fix the dependency issues instead of hiding them.
Performance Blind Spots
Let’s talk about those sneaky performance issues. They’re the kind that fly under the radar because everything seems fine—until it isn’t. Let’s uncover these silent culprits and see how to fix them.
The Problem
Here’s a component with some subtle performance pitfalls:
const ExpensiveList = ({ items, onItemClick }) => {
const processedItems = items.map(item => ({
...item,
fullName: `${item.firstName} ${item.lastName}`,
// expensive calculation lurking here
}));
const handleClick = (id) => {
onItemClick(id);
};
return (
<div>
{processedItems.map(item => (
<div key={item.id} onClick={() => handleClick(item.id)}>
{item.fullName}
</div>
))}
</div>
);
};
Can you spot the issues? Let’s break them down.
Fixes
1) Memoize Expensive Calculations
Instead of recalculating everything on each render, use useMemo to cache results:
const ExpensiveList = ({ items, onItemClick }) => {
const processedItems = useMemo(() =>
items.map(item => ({
...item,
fullName: `${item.firstName} ${item.lastName}`,
})),
[items]
);
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
{processedItems.map(item => (
<ListItem
key={item.id}
item={item}
onClick={handleClick}
/>
))}
</div>
);
};
const ListItem = memo(({ item, onClick }) => (
<div onClick={() => onClick(item.id)}>
{item.fullName}
</div>
));
This avoids recalculating data and recreating event handlers on every render. It also prevents unnecessary re-renders of child components with memo.
2) Efficient State Updates
Poor state management can also kill performance. Here’s a better way to handle updates like search results:
const SearchInput = () => {
const [results, setResults] = useState([]);
const debouncedSearch = useMemo(() =>
debounce((value) => {
fetchSearchResults(value).then(setResults);
}, 300),
[]
);
const handleChange = (e) => {
debouncedSearch(e.target.value);
};
return <input type="text" onChange={handleChange} />;
};
Debouncing ensures we’re not fetching data on every keystroke, reducing unnecessary API calls and re-renders.
Quick Performance Tips
- Don’t overuse memoization: Optimize only when it’s worth it.
- Avoid inline functions: Stable references improve performance.
- Keep props predictable: Shallow and stable props help components stay efficient.
- Break up large lists: Tools like react-window can handle big datasets gracefully.
- Move state closer: Only manage state where it’s actually needed.
- Profile with DevTools: Always measure before optimizing.
Conclusion
Building components isn’t rocket science, but let’s be real—it’s easy to stumble into bad habits. I’ve made every one of these mistakes (and sometimes still do). That’s okay. What matters is catching them early, fixing them, and avoiding rough codebase.
Quick Checklist for Better Components
✅ Single Responsibility: If you can’t summarize your component’s job in one sentence, it’s time to break it apart.
✅ Props Management: Passing props through layers upon layers? Consider using Context or leveraging composition instead.
✅ State & Effects: Keep effects focused, clean them up properly, and let modern tools handle complex data fetching.
✅ Performance: Don’t optimize for the sake of it—measure first. Use tools like memo, useMemo, and useCallback smartly when needed.
✅ Start Simple: Solve the problems you have now, not the ones you might have someday.
The best components aren’t flashy or overly clever—they’re the ones your team can read and maintain without groaning six months down the line.
Remember: these aren’t hard rules, just guidelines. You’ll break them sometimes, and that’s perfectly fine. The goal isn’t perfection—it’s building components that don’t make you question your career choices when you revisit them later.
Top comments (1)
i prefer leveraging composition, many tims context will going to context hell.