Your React components are doing too much. I can say this with confidence because after reviewing hundreds of codebases, I see the same pattern everywhere: components that started simple but gradually accumulated responsibilities until they became unmaintainable monsters.
The worst part? This happens so gradually that teams don’t notice until they’re already in pain.
The Smart Component Trap
Here’s how it usually starts. You build a UserProfile
component that fetches user data and displays it. Simple enough:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId)
.then(setUser)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
return (
<div>
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Six months later, that same component looks like this:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
const [preferences, setPreferences] = useState({});
const [notifications, setNotifications] = useState([]);
const [showNotifications, setShowNotifications] = useState(false);
useEffect(() => {
Promise.all([
fetchUser(userId),
fetchPreferences(userId),
fetchNotifications(userId),
]).then(([userData, prefs, notifs]) => {
setUser(userData);
setPreferences(prefs);
setNotifications(notifs);
setLoading(false);
});
}, [userId]);
const handleEdit = () => setEditing(true);
const handleSave = (data) => {
updateUser(userId, data).then(() => {
setUser(data);
setEditing(false);
});
};
const handleNotificationClick = (id) => {
markAsRead(id);
setNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, read: true } : n))
);
};
// ... 150 more lines of logic
}
Sound familiar? This component is now responsible for data fetching, state management, user interactions, notifications, preferences, and presentation. It’s become a “god component” that’s impossible to test, reuse, or modify safely.
The Real Cost of Smart Components
1. Testing Becomes a Nightmare
When components do everything, you can’t test individual pieces. Want to test the notification logic? You have to mock the entire user fetching flow. Want to test the edit functionality? Same problem.
2. Reusability Dies
Need a simple user display somewhere else? Too bad — your component comes with all the editing, notifications, and preferences baggage. You end up copying and pasting pieces instead of reusing components.
3. Performance Takes a Hit
Smart components often re-render unnecessarily because they’re subscribed to too much state. Change a notification status? The entire profile re-renders, including expensive user data processing.
4. Debugging Becomes Detective Work
When something breaks, you have to trace through multiple responsibilities to find the culprit. Is it the data fetching? State updates? User interactions? Good luck.
Here’s how I restructure that UserProfile
component:
1. Container Component (Smart)
Handles data fetching and coordinates between pieces:
function UserProfileContainer({ userId }) {
const { user, loading } = useUser(userId);
const { preferences } = usePreferences(userId);
const { notifications } = useNotifications(userId);
if (loading) return <LoadingSpinner />;
return (
<div>
<UserDisplay user={user} />
<UserEditor user={user} onSave={handleSave} />
<NotificationList
notifications={notifications}
onNotificationClick={handleNotificationClick}
/>
</div>
);
}
2. Presentation Components (Dumb)
Each focused on a single UI concern:
function UserDisplay({ user }) {
return (
<div>
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function UserEditor({ user, onSave }) {
const [editing, setEditing] = useState(false);
// editing logic only
}
function NotificationList({ notifications, onNotificationClick }) {
// notification display logic only
}
3. Custom Hooks (Business Logic)
Extract reusable logic into custom hooks:
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId)
.then(setUser)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading };
}
The Component Responsibility Checklist
Before you add any new feature to a component, ask:
- Does this component already have a clear, single purpose?
- Would this addition make the component harder to test?
- Could this logic be useful elsewhere in the app?
- Am I adding state that’s unrelated to the component’s main job?
If you answer yes to any of these, it’s time to extract.
Practical Refactoring Strategy
Don’t try to fix everything at once. Use this incremental approach:
Week 1: Identify the Worst Offenders
Look for components with:
- More than 10 state variables
- Multiple useEffect hooks doing different things
- Functions that don’t relate to the main purpose
- Files longer than 200 lines
Week 2: Extract Custom Hooks
Move business logic into custom hooks. This immediately makes logic reusable and testable.
Week 3: Split UI Concerns
Break complex render methods into focused presentation components.
Week 4: Create Container/Presentation Pairs
Separate data fetching from display logic.
The Long-Term Payoff
Teams that consistently apply this pattern report:
- 50% faster feature development (components are easier to modify)
- 80% fewer bugs (isolated responsibilities mean fewer side effects)
- Much easier onboarding (new developers can understand individual pieces)
- Significantly better test coverage (small components are actually testable)
Common Pushback and Responses
“This creates too many files!” — Yes, you’ll have more files. But you’ll also have files you can actually understand and modify confidently.
“It’s more overhead for simple features!” — True for the first implementation. But the second time you need similar functionality, you’ll reuse existing pieces and move much faster.
“Our designers won’t like the extra complexity!” — Your designers care about user experience, not file count. Better component architecture leads to more consistent UI and faster iterations.
Start Today
Pick your most problematic component. The one that makes everyone groan when they have to modify it. Apply this checklist:
- What are all the things this component does?
- Which of these could be custom hooks?
- Which parts are pure presentation?
- What’s the minimal coordination logic needed?
Extract one piece. See how much easier it becomes to work with. Then do it again.
Your future self (and your teammates) will thank you.