Every React codebase starts the same way: a handful of clean components, one obvious place to put state, a folder structure you could sketch from memory. Eighteen months later it’s a utils folder with forty unrelated files, three different ways of fetching data, and one dashboard component nobody wants to open. Nothing dramatic caused this. No single bad hire, no single bad PR. It’s the compound interest of small, individually reasonable choices that were never checked against a plan.
The scale of that drift is measurable. Stripe’s Developer Coefficient research found developers spend upwards of 17 hours a week on maintenance work, a meaningful share of it fixing bad code rather than shipping anything new, which Stripe extrapolates to roughly $300 billion in lost productivity across the industry each year.
React itself isn’t the villain here. It’s also not innocent. There’s no mandated folder layout, no built-in state layer, no enforced boundary between one feature and the next, the framework hands that decision back to the team. If the team doesn’t supply an answer, the codebase supplies its own, one component at a time. That’s architecture by accident, and it’s the default outcome unless someone actively prevents it.

The tax you don’t notice you’re paying
Complexity rarely arrives as one bad decision. It arrives as several good-enough ones:
- A prop gets threaded through four components because it’s “just one more prop.”
- A useEffect gets added to keep two pieces of state in sync, instead of deriving one from the other.
- A new feature lands in an existing folder because there’s no obvious home for a new one.
- A shared index.ts barrel file picks up one more re-export.
Each of these is defensible on its own. The research on cyclomatic complexity and defect rates backs up the intuition about what happens when they pile up: for serious, real-world bugs, complexity metrics track meaningfully with where defects show up, even though the relationship isn’t uniform across every category of bug. Complexity doesn’t guarantee bugs. It shrinks your margin for avoiding them and margin is precisely what a growing app can least afford to lose.
Build tooling feels the same pressure in a more literal way. Barrel files are convenient until the dependency graph behind them becomes the bottleneck: Atlassian’s engineering team cut build times by roughly 75% simply by removing barrel files, and Next.js has reported development-build gains of 15–70% when bundlers can skip barrel-style re-exports. The mechanism is unglamorous, importing one component through a barrel forces the bundler to parse everything else re-exported alongside it, and in a large codebase that “everything else” can mean tens of thousands of files touched for a single import.
It starts at the component level
React’s own documentation is unusually candid about where this trouble tends to begin, worth taking seriously, since it’s coming from the team that built the mental model in the first place.
Prop drilling and premature lifting
React’s guidance on sharing state is to lift it only as far as the components that actually need it, not further. In practice, state tends to get lifted to the nearest convenient ancestor rather than the correct one, and a value that started in one component ends up passed through three others that never use it. Multiply that by a few dozen features and no one can trace where a value actually comes from without opening five files.
Context reached for too early
React’s docs on passing data deeply are explicit that Context is a last resort after composition and prop-passing, not the first response to prop drilling. Used for frequently changing state, it also re-renders every consumer downstream, part of why so many mid-sized apps quietly bolt on a dedicated state library later.
Effects used to synchronize instead of derive
The React docs page You Might Not Need an Effect is arguably the most consequential one for long-term maintainability, because useEffect is the easiest tool in React for accidentally building hidden, order-dependent logic. An effect that exists purely to keep two pieces of state in sync is a small, undesigned state machine and those are exactly the bugs that are hardest to reproduce later.
Logic that never got extracted
Custom hooks exist to separate what a component does from how it does it. Skipping that step is how a component quietly grows to 600 lines doing five unrelated jobs, the classic “god component” new hires are afraid to touch.
State libraries are a symptom, not a fix
State management churn is one of the more visible signs of a codebase reacting to its own complexity rather than resolving it. The State of React 2025 survey found Zustand usage nearly doubled in two years, 28% in 2023, 41% in 2024, 50% in 2025, while plain Redux remains the most widely used option at 75.5%, down from 80.5% in 2023. About a third of respondents use no dedicated state library at all, relying on useState and useContext alone.
A new state library rarely fixes an architecture problem. It just moves where the problem lives.
Read generously, this reflects teams recognizing that server data, UI state, and cross-cutting app state have different lifecycles and don’t belong in one global box. Read less generously, it’s teams reaching for a new tool because the old state layer became unmanageable, without asking why.
Borrowed vocabulary for a framework with none of its own
Because React doesn’t prescribe structure, the patterns teams reach for once they outgrow components-and-utils almost all come from outside the React ecosystem:
- Feature-Sliced Design : organizes code by business feature first, technical layer second, the inverse of the traditional components/hooks/utils split that scatters one feature’s logic across the whole tree. It defines explicit layers (app, pages, widgets, features, entities, shared) with rules about which can import from which.
- Screaming Architecture (Robert C. Martin) : argues a codebase’s top-level folder structure should announce what the application does, not which framework built it.
- Presentation-Domain-Data layering (Martin Fowler) : separates what the user sees, the business rules governing the app, and the data underneath. Older than React, but a near-exact fit for the common failure mode of business logic leaking straight into components.
- Conway’s Law : system structure tends to mirror the communication structure of the organization that built it. A React app assembled by several teams without a shared architectural convention will often look exactly like that org chart, whether or not anyone intended it to.
None of these are React-specific. That’s the point, the framework doesn’t supply architectural opinions, so teams borrow them from software architecture generally.

What restructuring actually looks like
“Just adopt Feature-Sliced Design” is easy to say and vague in practice. A few real examples, drawn from how engineering teams have documented their own work:
- Slack rebuilt its testing layer rather than its component structure, for the same underlying reason most React refactors happen: an old pattern, Enzyme tests tightly coupled to component internals had become a liability as the codebase grew. Combining AST-based codemods with an LLM-assisted transformation step, the team converted more than 15,000 tests to React Testing Library at an 80% automated success rate.
- Shopify split its shared code out of one monolithic repository into multiple purpose-grouped monorepos, general purpose packages separated from performance sensitive ones because a single undifferentiated repo made it too hard to reason about what depended on what.
- Atlassian’s barrel-file removal wasn’t cosmetic. It was driven by build times that had degraded enough to actively slow development, and the fix was structural changing how modules were exported not more caching or hardware.
The common thread: none of them fixed the problem by rewriting components. They fixed it by changing the rules governing how components and modules relate to each other. That’s an architecture decision, not a component decision.
This shows up in delivery metrics, not just code review
It’s tempting to file maintainability under “code quality” and delivery speed under a separate heading. DORA’s research ties the two together directly. In the 2024 State of DevOps Report, elite-performing teams held change failure rates around 5% and lead times for changes under a day, with the fastest teams measuring lead time in minutes. The same report flagged a worrying shift: the high-performer cluster shrank from 31% of respondents to 22% year-over-year, while the low-performer cluster grew from 17% to 25%.
DORA’s 2025 research on AI-assisted development adds a sharper edge to that finding. Based on responses from nearly 5,000 technology professionals, roughly 90% now use AI tools daily and the report’s central conclusion is that AI amplifies whatever is already true about a team’s underlying systems. A well-architected codebase gets AI-assisted changes landed faster and more safely. A tangled one gets AI-generated changes that compound the tangle just as fast as a human would have, only quicker.
Architectural drift isn’t just harder to work in, it’s a ceiling on how much faster any tooling, AI-assisted or otherwise, can make your team.
Where to start, without a rewrite
None of this requires starting over. It requires naming the boundaries that already implicitly exist in the codebase, before they calcify into something worse:
- Audit where state actually lives against where it’s actually used. Prop drilling and premature Context are both symptoms of state sitting one level too high or too low.
- Read every useEffect as a to-do list item. One synchronizing two pieces of state is a candidate for derived state instead.
- Group new work by feature, not by file type. You don’t need to migrate the whole tree at once to stop the bleeding.
- Treat build-time regressions as architecture signals, not just tooling problems. A slow build is often a dependency graph describing coupling you haven’t noticed yet.
- Pick a state tool for what it actually solves server cache, UI state, or cross-cutting app state, instead of reaching for one tool to cover all three.
A React codebase doesn’t become unmaintainable because of one bad call. It becomes unmaintainable because hundreds of small, reasonable, component-level decisions were never checked against an architecture built to absorb them. The fix isn’t more discipline at the component level, it’s giving the codebase a shape that makes the disciplined choice the easy one.
You may also like : AI Agent Statistics You Need to Know in 2026
