Your Component Is Doing Too Much: Finding the Seams
The 600-line component isn't a size problem, it's a responsibility problem. How to split data from presentation, and why 'reusable' components are often welded to one screen.
There's a file in every codebase I've ever worked in. You know the one. Ours was called UserDashboard.tsx, it was 640 lines long, and touching it was a team ritual: you'd open it, scroll for a while, sigh audibly, and make your change as surgically as possible while praying you didn't disturb anything.
It fetched data from four endpoints. It managed eleven pieces of state. It contained three modals, a filter bar, inline styles, a date-formatting helper, and — my favorite — a hand-rolled debounce. Every feature that landed anywhere near the dashboard got bolted onto it, because where else would it go?
Nobody set out to build that. It accreted, one reasonable-looking PR at a time. Which is exactly why component boundaries are a review concern, not just an architecture concern: the 640-line monster is prevented in the diffs, or not at all.
Size is the symptom; responsibility is the disease
The line count isn't really the problem. I've seen 300-line components that were perfectly fine (a big form, cohesive, doing one job) and 80-line components that were a mess. The real question is: how many reasons does this component have to change?
UserDashboard.tsx changed when the API changed, when the design changed, when the filter logic changed, when the modal flow changed, and when the date format changed. Five teams' worth of reasons in one file means constant merge conflicts, terrified refactoring, and tests that mock half the universe.
The classic seam — and still the most useful one — is data versus presentation:
- Data concerns: fetching, caching, mutations, URL params, permissions, business rules.
- Presentation concerns: layout, styling, interaction states, accessibility, animation.
// The seam, made explicit
function UserDashboard() {
const { user, isLoading, error } = useUser();
const { invoices } = useInvoices(user?.id);
if (isLoading) return <DashboardSkeleton />;
if (error) return <DashboardError onRetry={refetch} />;
return <DashboardLayout user={user} invoices={invoices} />;
}
DashboardLayout and everything below it takes data as props and knows nothing about where it came from. You can render it in Storybook with fixture data, test it without mocking fetch, and hand it to a designer-engineer to polish without them needing to understand your caching strategy. The hooks above it can change their data source entirely without touching a single pixel.
You don't need a formal "container/presenter" pattern or extra folders. You just need the seam to exist.
The "reusable" component that's welded to one screen
Here's the opposite failure mode, and honestly the sneakier one. Someone builds <Card>, and it's lovely. Then the orders page needs a badge, so <Card> grows a showBadge prop. Then checkout needs a different footer: footerVariant="checkout". Eighteen months later:
<Card
showBadge
badgeType="warning"
isOrderCard
hideFooterOnMobile
onSpecialClick={handleOrderClick}
legacyPadding
/>
This component is "reused" on five screens, but it isn't reusable — it's five components sharing a body. Every boolean prop is a fork in the road, and the number of paths through the component doubles each time. Nobody can change it without testing five screens they've never seen.
Two signals I watch for in review:
- Props named after a consumer.
isOrderCard,checkoutMode,forAdminPanel. The moment a shared component knows who is using it, the abstraction has failed. The fix is usually composition: give the componentchildrenor named slots, and let each screen supply its own specifics. - A new boolean prop on an already-busy shared component. Each one seems tiny in isolation. The question I ask is: "would it be simpler for this screen to have its own small component that composes the shared pieces?" Very often, yes.
// Instead of a config-flag jungle, expose slots
<Card>
<Card.Header badge={<Badge tone="warning">Overdue</Badge>}>
Order #4921
</Card.Header>
<Card.Body>{children}</Card.Body>
<Card.Footer>{/* each screen brings its own */}</Card.Footer>
</Card>
A good shared component is opinionated about appearance and agnostic about content. When those flip — flexible appearance, hardcoded content assumptions — you've built a screen fragment, not a primitive.
How boundaries erode, one PR at a time
The failure is almost never "someone wrote a giant component." It's "someone added 30 lines to a component that was already at the edge." Each individual diff looks harmless, which is why I try to review the resulting file, not just the added lines. GitHub shows you the diff; the monster lives in the whole.
A few review prompts that catch erosion early:
- Can I describe this component's job in one sentence, without "and"? "Renders the invoice table" — great. "Renders the invoice table and manages the export flow and syncs filters to the URL" — three components wearing a trench coat.
- Does the new code share a reason-to-change with the code around it? A date formatter added inside a component will be needed elsewhere within a month. Pull it out now, while it's cheap.
- Could this new piece render in isolation? If extracting it would require threading twelve props through, the state is living too high or the piece is cut in the wrong place.
This has become more important, not less, with AI assistants in the mix. When you ask an assistant to "add an export button to the dashboard," it does exactly that — inline, in the dashboard, right where the cursor is. It optimizes for the diff that satisfies the request, not for the file's long-term shape, and it will happily add the fourteenth boolean prop to <Card> because the existing thirteen made it look like the house style. Generated code amplifies whatever patterns already exist. If the boundary discipline isn't coming from the codebase, it has to come from the reviewer.
Where to cut: a practical heuristic
When a component needs splitting, people often freeze at "but where?" My default order:
- Split data from presentation first. Hooks up top, dumb layout below. This one seam pays for most of the benefits.
- Extract anything with its own state cluster. If five
useStatecalls only ever change together for the modal, the modal wants to be its own component. - Extract anything you wish you could Storybook. That instinct — "I'd love to see this piece in isolation" — is your architecture talking.
- Stop. Don't shatter it into confetti. A tree of 15 two-line components is its own kind of unreadable. Boundaries should trace responsibilities, not line counts.
Takeaways for your next review
- Judge components by reasons-to-change, not lines.
- Keep one visible seam between "where data comes from" and "how it looks."
- Treat consumer-specific props on shared components as a design smell; reach for composition and slots.
- Review the resulting file, not just the diff — erosion is invisible in +30/-2.
- Expect AI-generated additions to mirror existing patterns, good and bad; the reviewer holds the line.
The 640-line dashboard eventually got split, by the way. It took two people a week. Catching it in review would have taken a sentence: "should this live here?" Cheap question. Ask it often.