
How Over-Modularity Is Hurting Frontend Development
In most React and Next.js codebases today, there’s an unspoken orthodoxy: components must be small. If a file grows beyond 250 lines, alarm bells ring and the team insists it be split up or “abstracted.” On the surface, this promises tidiness and modularity. Entire folder structures get built around micro-components and index.ts
files that re-export them.
This drive for smallness comes from sensible ideals: readability, reusability, and testability. Clean Code, Atomic Design, and years of “divide and conquer” wisdom have shaped how React teams structure their code. Tutorials, guides, and community forums reinforce this: larger files are treated as problems to be broken down into supposedly reusable pieces.
Barrel files, simple index.ts
files that re-export multiple things, quickly became the popular way to flatten imports and hide internal structure. Instead of writing a long path like import Button from '@/components/Button/Button.tsx'
, you get import { Button } from '@/components'
. Many project templates, especially older Next.js and so-called “bulletproof” React architectures, encouraged this liberal use of barrels to present a clean interface.
It’s 2025, and most of us are knee-deep in codebases full of tiny files, barrels, and layers on layers of abstraction. New hires are onboarded into this philosophy. If your React component doesn’t fit on one screen, it must be refactored. Teams pride themselves on highly granular code structure, always in the name of clarity and maintainability.
The culture rewards anyone who follows these rules. Junior developers are taught to fear the god component, a giant file doing too much, as the ultimate sin and instead to slice features into many bite-sized pieces.
But has it worked?
This article takes a hard look at how these prevailing norms, the tyranny of ever-finer modularity, may be backfiring. We’ll examine the unintended costs: navigation fatigue, cognitive overload, slowed development, and even performance hits. We’ll also look at how other ecosystems (Go, Rust, Python) strike a different balance. The goal isn’t to swing the pendulum to the opposite extreme but to find a saner middle ground.
Origins of the 250-Line Rule and Micro-Components
Why did we come to revere the sub-250-line React component?
It started with classic software advice: keep things small and focused. Object-oriented design’s “single responsibility principle” said every piece should do one thing. The UI world embraced this, teaching that each component should handle a single concern (just one slice of the UI or a self-contained bit of logic).
In early React, this took shape as the split between presentational and container components, separating “dumb” UI from “smart” stateful logic. That naturally pushed codebases toward more, smaller components and away from monolithic files.
Then came Clean Code, which championed small functions and classes. In React circles, that spirit quietly became an informal “cap” of around 250 lines per component. It’s a number you’ll hear echoed in code reviews and team chats, never written in stone but treated as gospel. Over time, teams began treating these thresholds as hard rules, even though they started as just rough heuristics.
Another driver was tooling and aesthetics. Modern IDEs and code reviews feel easier when each file is short and focused. A 100-line file feels approachable; a 1000-line one can intimidate. GitHub diffs are easier to review when files are small. There’s also a psychological pull: developers feel a sense of “cleanliness” when a project is organized into dozens of bite-size files, each with a clear purpose. It looks modular and elegant (code with good “feng shui”).
Frameworks and tutorials reinforced this. Next.js doesn’t require line limits, but file-based routing and now React Server Components nudge you toward splitting code. In the App Router, every route includes separate files for layout, page, loading state, error boundary, and more. The architecture pushes for many small, decoupled components (especially with server components, where logic and view often split across files).
Open source set the trend, too. Design systems like Shopify’s Polaris and frameworks like Material-UI showcase lots of small components, meticulously split into their own files, often with barrels to export them. Seeing this, application developers mimic the style: “If my code looks like a design system, it must be enterprise-grade.” Sometimes it’s just cargo-culting (adopting patterns without checking if they actually fit the context).
Let’s be clear: modularity itself isn’t the problem. It’s taking good ideas to extremes and turning sensible rules into rituals, stripped of their purpose. When we split everything into tiny pieces just because a rule says so, the complexity doesn’t disappear. It just moves into the connections between files.
“The limit isn’t lines of code, it’s complexity. A 50-line component juggling 10 pieces of state and props can be much harder to follow than a 300-line component with a simple, linear flow.”
So how does this get taught? Usually by example. Open-source starter repos and guides (like Bulletproof React) push feature folders with index files and one component per file. Juniors learn to start each component with a new file, even if it’s just a few lines of JSX. Some teams go further, baking line limits into code review or lint rules. Over time, the mindset sets in: if in doubt, split it out.
It spreads beyond UI code, too. Utility functions, hooks, and context providers are isolated in their own modules. Even experienced developers who warn about “wrapper hell” feel pressure to keep up with “best practices.” Nobody wants to be the one who writes a 500-line React component and gets shamed for it in a pull request.
Barrel Files: Convenience vs. Complexity
Alongside the micro-component trend came the rise (and eventual overuse) of barrel files, usually named index.ts
or index.js
.
A barrel file simply re-exports items from other files, acting as a single entry point for related modules. Instead of importing like import Button from '@/components/Button/Button.tsx'
, you can write import { Button } from '@/components'
. This hides internal file structure and, in theory, presents a tidy, modular interface.
Barrel files seemed like a harmless improvement. Frameworks like Angular helped popularize them, and TypeScript’s own docs once showcased index barrels. Many React style guides (some I even contributed to) recommended grouping feature modules and exposing only their “public” components this way. On large teams, barrels promised a way to enforce boundaries: you could reorganize internals without breaking imports elsewhere. The barrel acted as the API for each folder.
But the convenience has a cost.
Overuse creates hidden performance, tooling, and cognitive issues. Developers and tool authors now warn: barrels often hurt more than they help. The biggest problem is with modern bundlers like Webpack or Vite. If you import just one thing from a barrel, you may end up pulling in everything it re-exports. Bundlers can’t always safely tree-shake dead code, especially when side effects are possible.
If you import {A}
from a barrel, you might get B, C, D, and E as well, bloating your bundle.
The result: shorter import paths, but potentially a lot of dead code in your shipped bundle. This has become such a common issue that Vite’s documentation now warns against barrels for performance reasons.
Barrels create other headaches too. Testing tools like Jest often don’t know which export you actually need, so they load every file referenced by the barrel, even if your test only touches one. As a result, importing a single component from a large library barrel can unintentionally initialize hundreds of modules, causing your test suite to slow down significantly.
In the past few years, developers have shared real results after removing barrel files from Next.js projects. One team cut their JavaScript bundle by 5 to 10 percent and shaved 25 percent off CI builds with direct imports. Dominik Dorfmeister, a TanStack maintainer, dropped loaded modules from 11,000 to 3,500 on some pages and saw load times improve right away. Each barrel file may seem harmless, but together they slow down everything from bundling to testing.
Beyond performance and testing, barrels introduce subtler risks. They can cause circular dependencies: say, if a utility in a barrel imports something else from the same barrel, you can trigger import cycles that produce confusing, intermittent bugs. Even without actual breakage, barrels add a layer of indirection. Developers have to hop into the barrel file to find out where a given symbol comes from. That context-switching builds up to navigation fatigue. Most barrels contain only export lines, so developers often ignore them, making the codebase’s public surface more implicit than intended.
As the downsides became clearer, the industry started pulling back. Once-trendy barrels now earn blog post titles like “Please Stop Using Barrel Files.” Next.js even introduced an optimizePackageImports
feature, but it mostly targets barrels in external packages, not your local index.ts
files. And even then, it only works if the barrel file does nothing but re-export. If you add a single line of logic or side effect, tree-shaking and direct imports can’t work, so the performance benefit disappears.
So, should you ever use barrels? Sometimes they make sense for public APIs of library packages, where you want a single entry point. But in application code, barrels mostly create extra overhead.
…if you are writing app code, you’re just making your life harder by putting index.ts files into arbitrary directories. So please stop using barrel files — Dominik, one of TanStack’s maintainers
The pain points are clear: slower tests, unpredictable bundling, and subtle import cycles, all for the sake of slightly shorter import lines. With editors handling autocomplete and path aliases, there’s rarely a good reason left to add another barrel.
The barrel file saga shows how a tidy idea can quietly spiral into trouble. What looked neat in small doses, at scale, piles up new complexity and hidden costs. Each layer of abstraction in code has a price, and dozens of barrels compound it in both performance and mental overhead. The same pattern plays out when teams split logic across too many files and components. The burden of “lots of small pieces” creeps in quietly. Sooner or later, it’s everywhere.
Fragmentation Fatigue: The Cost of Countless Small Files
The developers in large front-end codebases shaped by today’s best practices often run into the same pattern when building something new. Take a seemingly simple task, like adding a dynamic filter to a table. If the setup were straightforward, you would open the Table component, find the right section, make a change, and move on. One or two files at most.
But that’s not usually how it plays out. Instead, you open Table.tsx
and see it just pulls in other bits: TableHeader
, TableBody
, maybe something called FiltersPanel
. Each one sits in its own file. Then you realize state lives in a custom hook somewhere else. Filter logic is stashed in a utility or handled by a context provider. All these pieces get re-exported through a barrel file, which adds another stop to the journey. To figure out what’s going on, you end up jumping from Table.tsx
to FiltersPanel.tsx
, then to useFilters.ts
and filters.utils.ts
. After a few of these jumps, you start to lose the thread, and the logic just feels scattered.
Reviewing this kind of setup is even more tiring. A change that sounds small, like tweaking table filters, might drag you through ten files spread across five different folders. By the time you’re done, you’re lucky if you can remember where you started. The reviewer jumps from file to file, trying to rebuild the story. By the end, the details from the start are already a memory.
Admittedly, I’ve lived through this from both sides. More than once, I’ve had to touch twenty-something files just to add a new filter. What should have been a quick extension of an existing component stretched into hours of sorting through patterns and piecing together dozens of tiny modules. First as the person writing the code, then as the person reviewing it.
Each file makes sense when you look at it alone. But together, they turn the codebase into a scattered puzzle. What started as a clean structure now feels like work just to follow along. Something meant to save time ends up costing everyone more. Simple changes reach into corners you didn’t expect. You get through it, but you’re left wondering why a small update needed to be this complicated.
Cognitive Overload from Too Many Abstractions
The biggest issue with over-modularization is cognitive overload.
Every new module is one more piece a developer has to keep in mind. In a codebase built from hundreds of “Lego pieces,” the effort to understand even a simple feature balloons quickly.
John Ousterhout, in A Philosophy of Software Design, draws a clear line: a few “deep” modules (each with real logic and a simple interface) are much easier to work with than dozens of “shallow” ones that just shuffle logic around. Too many shallow modules mean you have to remember not only what each part does but also how all the pieces fit together.
The cognitive-load repo on GitHub gives a clear example. It compared two similar codebases, each about 5,000 lines. One was split into 80 tiny classes and modules, the other into just seven larger ones. After a year away, it was nearly impossible to reload the context for the first. The second was easy to pick up. Deep modules made the difference.
In React, it’s the same story. Ten files for one UI feature can be much harder to reload than a single, well-organized file. Even if it’s longer. More modules do not always mean more maintainability, especially if each one is too granular. The human brain can keep track of seven moving parts much more easily than eighty.
How Other Ecosystems Balance Modularity and Coherence
“Flat is better than nested” — The Zen of Python
Looking at ecosystems like Go, Rust, or Python, it’s clear they prioritize meaningful grouping over rigid line-counting. Modularity in these communities means organizing code by logical coherence rather than enforcing arbitrary file size limits.
Go prefers small packages organized by business logic or domain. Files within these packages often exceed 300 or 400 lines if clarity calls for it. Go handles public and private scope through naming, not artificial folder splits. You won’t find barrel files here.
Rust follows a similar path. Projects usually begin as a single file and split naturally along meaningful seams as complexity arises. Rust developers care about clarity, coherence, and logical separation, not arbitrary file lengths.
Python is pragmatic: flat beats nested. Large modules are fine if they’re straightforward. Python guides discourage overly granular functions and tiny modules. Since there’s no tree-shaking in Python, developers keep imports direct and local, avoiding unnecessary complexity and startup overhead.
Even frontend frameworks weren’t always this granular. Early Backbone or Angular apps grouped code by feature, not file size. A 500-line file handling user registration was common. It may be messy, but it’s straightforward.
The takeaway from these communities isn’t to embrace huge, unruly files. It’s to resist splitting code by rote. Modularity means high cohesion (grouping what changes together) and low coupling (independent, clearly defined parts).
“Don’t abstract until you have three real use cases.”
Practical teams delay abstraction. Wait until the third real use case emerges before generalizing. If you use a utility once, keep it local; when you copy it for a third time, then generalize. This avoids up-front complexity and saves time guessing what might be reusable.
A thoughtfully structured monolith, with isolated modules, is often easier to work with than a thicket of microservices. The same is true in code: a single large component with clear internal boundaries can be easier to maintain than a web of mini-components and wrappers. If a component grows too large, you can always split it later, when you see a real need.
Take forms as an example. In Django, you might write a single class or function for a multi-step wizard, all in one file, flowing top to bottom. In React, the same feature might get split into five or six components, each with its own file and logic scattered. The Django approach might run 500 lines but feels readable; the React version means piecing things together across files.
It’s not that Python or Go developers are smarter; their culture just prizes practicality and readability. The JS world is finally catching up: do what actually works for your team, not what a style guide or “best practice” blog says.
“Focus on minimizing cognitive load. That’s what makes code truly maintainable.”
Toward a More Balanced Modularity
“Big files aren’t always a red flag, and small ones aren’t automatically easier. The real question is, do you understand it?”
Most frontend teams, at some point, swing way too far into splitting things up. Getting back to sanity doesn’t mean going all-in on god components. Here’s what actually works in practice:
Stop treating line numbers as gospel.
Forget the 250-line myth. Big files aren’t always a red flag, and small ones aren’t automatically easier. The real question is, do you understand it? Sometimes 300 lines with a clear flow beats 100 lines full of cross-file indirection. If it’s easy to scroll and you can explain it to a teammate, it’s probably fine.
Keep things that change together close together.
If a helper, a hook, or a subcomponent only matters to a feature, just keep it in the same file. You don’t need to stick to “one component per file” for its own sake. High cohesion isn’t about folder structure. It’s about not scattering logic all over just because a rule says so. That form and its validation schema? There’s no harm in putting them side by side.
Abstraction isn’t free.
Every extra wrapper, extra file, or function hop makes things harder to follow. Don’t invent layers unless you’re solving a real problem, like genuine code sharing or simplification. Some days, handling a click means jumping through four files. Sometimes, you’re better off inlining the logic and calling it a day.
Barrel files? Most apps don’t need them.
Explicit imports are your friend. Use barrels only when you’re exposing a real public API, and don’t mix in other logic. Most of the time, autocomplete and path aliases cover the “long import” problem anyway. If skipping barrels means a few longer import lines, that’s a fair trade for not dragging in extra code or confusing your bundler.
Write for your team, not a tutorial.
Fancy abstractions look great in talks and blog posts, but they can make real maintenance a slog. Ask yourself, is this helping future you or just making things look “clean” for someone else? Duplication is sometimes cheaper than another shared module you’ll regret later.
Let your tools and your team help.
Modern IDEs do half the work: folding, jump-to, and outlines. Use them. And talk with your team. If everyone hates opening twelve files to fix a bug, that’s a hint something’s off. Don’t let CI or lint rules make the decisions for you. When you do change structure, add a quick note explaining why, so nobody has to guess in six months.
Clarity is always the goal. Split code when it helps; keep it together when that’s clearer. Ignore dogma and keep an eye on how much you can actually hold in your head. That’s the real test.
Clarity Over Dogma
Every few years, the software industry reconsiders its cherished dogmas. Over-engineered enterprise code gave way to simpler frameworks; the microservices craze is now giving way to something more measured.
Today, frontend development is at the same kind of crossroads.
The current norms (ultra-small React components, endless splitting, barrels everywhere) were meant to tame complexity. But taken to extremes, they just create new complexity. Now, the pain shows up as slow progress, hard onboarding, and mounting frustration as teams wander a maze of “clean” code that’s anything but clear.
It’s time to push back against arbitrary rules. Real-world software is messy. Forcing everything into neat little boxes often makes the system messier. What we need are coherent, substantial pieces that make sense internally and don’t overwhelm with sheer quantity.
This isn’t an argument against modularity. It’s a call to modularize with intent. Modules should be as small as needed for clarity, but no smaller. The art is in finding that Goldilocks zone where every part is understandable and self-contained, but not so tiny it loses context. Aim for a system where the pieces form a true whole.
Frontend developers can learn from classic design ideas like information hiding, coupling, and cohesion, and from modern thinking about cognitive load. Measure your codebase not by how strictly it follows patterns, but by how easily you can ship improvements and how confidently you can evolve.
When you run into a “best practice,” don’t accept it blindly. Ask why it exists, who it benefits, and what the trade-offs are. The evidence is clear: barrels can hurt performance and clarity, too many tiny modules can overload your team, and “clean” code can be an illusion that hides real complexity.
It takes a little courage to break with prevailing styles, especially if you were taught to see them as “the right way.” But the reward is code that feels refreshingly simple and teams that are more productive, not less. More and more developers report that simplifying their code, even if it means writing a 400-line component, leads to faster progress and no loss of stability. They stopped writing code to impress and started writing code to express.
Maybe it’s time to remember why we loved React and JavaScript in the first place. Not because they forced us to write tiny components or use barrels, but because they gave us flexibility to build rich interfaces, to compose as needed, and to keep things reasonable. We may have overcomplicated things chasing an ideal. Dialing back complexity gets us closer to the real goal: making programming easier.
Forget dogma. Build what you can actually understand. If a pattern actually makes your life easier, use it. If it’s just more steps, ditch it. We need fewer commandments and more honest questions.
“Does this make things clearer?”
“Will the next developer know what’s going on?”
The goal isn’t to impress a style cop. It’s to write code that you, your teammates, and future maintainers can pick up, follow, and change without feeling lost. If that means skipping a barrel file or putting two tiny components in one file, so be it.
Methods change. Today’s gospel can feel clumsy tomorrow. What matters is that our code actually works for us. Build UIs you’ll want to revisit in six months. If you can come back, see the flow, and make a change without a headache, you’re on the right track. That’s enough.
Ship what makes sense. Refactor when you need to. Enjoy the sanity.
Viz
Become a subscriber receive the latest updates in your inbox.
Member discussion