DEV Community

Cover image for Why your “simple” react app exploded
<devtips/>
<devtips/>

Posted on

Why your “simple” react app exploded

How a tiny feature quietly turned into a frontend ecosystem and why that’s starting to feel wrong

It always starts with a lie. Not a malicious one. A friendly, optimistic lie.

“This is just a small feature.”

You spin up a React app because… well, that’s what you do now. You need a page, a form, maybe a list. Nothing fancy. You’re not building the next Figma. You’re just trying to let users update a couple of fields and move on with their lives.

At first, everything feels fine. Clean components. A hook or two. You’re productive. Confident. Slightly smug.

Then things start accumulating.

You add routing because it’s “basically free.”
A data-fetching library because fetching in useEffect feels wrong now.
Global state because passing props feels like a code smell.
Auth wrappers. Loading states. Error boundaries. Skeletons. A build step that takes longer than the feature itself.

None of this feels like a mistake while you’re doing it. That’s the problem.

A week later, you realize you’re maintaining more infrastructure than product. A button click triggers a cascade of hooks, context providers, caches, and re-renders that require browser devtools and emotional resilience to debug. The feature works but it feels heavy. Touchy. Fragile in ways you can’t quite explain.

That was the moment I snapped.

Out of frustration and curiosity, I rewrote the same feature using HTMX. No framework ceremony. No client-side state gymnastics. No JavaScript bundle having a meltdown before rendering HTML.

It worked immediately. Which felt… unsettling.

TL;DR
This isn’t a React hate post. React solved real problems and still does. This is a story about how “simple” frontend work keeps turning into full-blown systems, why that’s happening, why tools like HTMX suddenly feel refreshing, and how to choose better defaults without swinging to the opposite extreme.

How react apps quietly become complicated

React doesn’t punch you in the face with complexity. It taps you on the shoulder and politely asks if you’d like to prepare for the future.

On day one, everything feels reasonable. You’ve got a component, some props, maybe a hook. The mental model is clean. You tell yourself this is the grown-up way to build UI now. Structure is good. Future-you will be grateful.

Future-you is grateful. Briefly.

Then real-world requirements start trickling in.

You add routing because “it’s basically free.”
You add a data-fetching library because fetching inside useEffect feels cursed now.
You add global state because passing props more than two levels deep feels like a moral failure.

Each step makes sense. That’s why this is so dangerous.

The problem isn’t any single decision it’s the stacking. Every new abstraction brings its own rules, its own failure modes, its own mental overhead. Suddenly, a button click isn’t just a button click. It’s a small event-driven system.

Here’s a painfully familiar example. Submitting a form in React often looks like this:
JSX:

const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function submit(data) {
setLoading(true);
setError(null);
try {
await api.save(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}

Totally fine. Totally normal.

Now add:

  • Validation
  • Optimistic updates
  • Cache invalidation
  • Disabled states
  • Error recovery
  • A loading skeleton that doesn’t flicker

None of that is wrong. But notice what happened: the infrastructure around the form is now larger than the form itself.

At some point, you’re no longer thinking about what the user needs. You’re thinking about render cycles, dependency arrays, and whether this state should live here or one provider higher. You open React DevTools not because something is broken, but because you’re not sure what’s happening anymore.

I’ve seen tiny internal tools accumulate:

  • Routing libraries for two routes
  • Global state for three values
  • Memoization for performance issues that never actually appeared

And every choice was made responsibly. That’s the part nobody likes to admit.

The cost doesn’t show up immediately. It sneaks in later:

  • Onboarding takes longer
  • Small changes feel risky
  • Nobody wants to touch “that” component

You don’t notice the explosion while it’s happening. You notice it when adding a checkbox feels weirdly stressful. When a “small tweak” turns into a careful, surgical operation.

That’s usually when the forbidden question surfaces:

Why does this feel so heavy?

Once that thought lands, it doesn’t go away.

Why frontend stacks keep getting heavier (and why it’s not your fault)

This part matters, but it doesn’t need to be long.

Frontend stacks didn’t get heavy because developers lost their minds. They got heavy because React worked.

SPAs solved real problems. They made the web feel fast, rich, and app-like. Entire products and careers were built on that shift. So we did what engineers always do when something works: we optimized around it.

We standardized patterns.
We wrapped sharp edges.
We turned hard-earned lessons into defaults.

And slowly, those defaults stopped being optional.

Today, you don’t decide to add routing it’s already there.
You don’t ask if you need a data layer the template assumes you do.
You don’t question client-side state it’s just how things are done.

That’s not bad engineering. That’s ecosystem gravity.

Frontend also borrowed a mindset from backend and infra: design for scale early. Which makes sense… until you apply it to a settings page used by a handful of people. We start solving imaginary problems for imaginary traffic because every blog post, talk, and repo example tells us to.

So we ship:

  • Build steps before users
  • Caching strategies before bottlenecks
  • Abstractions before understanding

None of this explodes immediately. It just adds weight. Mental weight. Maintenance weight. The kind that makes small changes feel bigger than they should.

Eventually you look at a basic feature and realize you’re maintaining a miniature system to support it.

That’s usually when people start looking for tools that feel… lighter.

Not newer.
Not smarter.
Just lighter.

What htmx changes and why it feels wrong in a good way

HTMX doesn’t feel innovative at first. It feels like you accidentally time-traveled.

There’s no “app.” No client-side state store. No hydration step holding the page hostage while JavaScript figures itself out. You write HTML. The server sends HTML. The browser swaps HTML. That’s it.

And if your React brain is screaming “that can’t be enough,” congratulations that’s the conditioning talking.

Here’s the mental flip HTMX forces on you: HTML is the API again. The server owns state. The browser just asks for fragments and swaps them into place. No JSON gymnastics. No “loading vs success vs error” state machine living in your head.

Compare the vibes.

React-style fetch:

useEffect(() => {
fetch("/profile")
.then(res => res.json())
.then(setProfile);
}, []);

Perfectly normal. Also now you’re thinking about effects, dependency arrays, rerenders, and where this state should live.

HTMX-style:

<div hx-get="/profile" hx-trigger="load" hx-swap="outerHTML">
Loading...
</div>

That’s the whole thing. The server returns HTML. The browser swaps it. You move on with your life.

The first time this works, it feels suspicious. Like you forgot something important. There’s no build step. No bundler. No “just one more library.” You refresh the page and it still works. You open devtools and… nothing exciting is happening.

Which is kind of the point.

HTMX doesn’t remove complexity it moves it. Back to a place we already know how to reason about: the server. Requests come in. Responses go out. Logs make sense again. Debugging feels familiar.

It’s not magic. It’s not new. It just refuses to participate in the modern assumption that every interaction needs a frontend framework to mediate it.

That’s why it feels wrong.
And why, for a lot of everyday features, it feels incredibly right.

The hidden costs nobody tweets about

Here’s the part that usually gets skipped in framework debates: nothing here is free.

React’s cost isn’t just money. It’s cognitive. Every abstraction you add takes up a little space in your head. Hooks, effects, providers, caches, invalidation rules none of them are hard alone. Together, they quietly raise the floor of how much context you need just to make a change.

That’s why onboarding slows down. New devs don’t struggle with syntax they struggle with why things are wired the way they are. You end up explaining patterns instead of problems. Rerenders instead of requirements.

HTMX flips the bill, but the bill still arrives.

By pushing logic back to the server, you’re accepting different tradeoffs. More server load. More round trips. More responsibility on templates and controllers to stay clean. If your backend is already a mess, HTMX won’t save you it’ll expose you.

Caching gets interesting again. Not glamorous interesting. The kind where you have to think. Fragment responses. Partial invalidation. CDNs that actually matter.

The difference is where the complexity lives.

With React-heavy apps, complexity spreads horizontally across components, hooks, and client state. With HTMX-style apps, it stacks vertically in the request–response flow. Some teams prefer one. Some prefer the other. Neither choice is morally superior.

What matters is that you see the cost before you commit.

If you pretend HTMX has no downsides, you’re lying.
If you pretend React is “just UI,” you’re also lying.

The mistake isn’t choosing the wrong tool.
It’s choosing without acknowledging what you’re paying for.

Where htmx completely falls apart

HTMX is great right up until it very obviously isn’t.

There’s a category of problems where pretending HTML swaps are enough turns into self-harm. If your app lives and dies by rich, continuous client-side interaction, HTMX will fight you the entire way.

Real-time collaboration is the big one. Shared cursors. Live presence. Multi-user editing. The kind of stuff where state changes constantly and latency needs to be invisible. Trying to model that with request–response fragments is like playing a shooter over email.

Same story with heavy client-side interactions. Drag-and-drop builders. Canvas-based tools. Complex animations tied to local state. You can duct-tape HTMX into these scenarios, but at that point you’re rebuilding the thing you were trying to avoid.

Offline-first apps are another hard stop. If your UI needs to function without a server round trip, HTMX is the wrong tool. There’s no shame in that. Different constraints, different answers.

Here’s the tell: the moment you start missing local state badly, you’re outside HTMX’s comfort zone.

<!-- This is where HTMX starts to struggle -->
<div draggable="true">
<!-- complex interaction, local state, no server round trip -->
</div>

That doesn’t mean HTMX is weak. It means it’s honest. It’s optimized for boring, transactional UI forms, dashboards, admin panels, internal tools. The stuff most of us actually build most of the time.

The mistake is turning that honesty into ideology.

HTMX isn’t here to replace React. It’s here to remind you that not every problem is an SPA problem. When you force it beyond that, it stops feeling elegant and starts feeling stubborn.

And that’s fine.

The future isn’t anti-react it’s anti-bloat

The internet loves a framework war, but this isn’t one.

React isn’t going anywhere. It’s still an incredible tool for the kinds of problems it was designed to solve. Rich interactions. Long-lived client state. Apps that feel closer to native than web. None of that suddenly became fake.

What is changing is the default.

More teams are realizing that not every page needs to behave like an app. Not every interaction needs a client-side state machine. Not every feature deserves a full frontend stack just because it’s available.

You can see this shift everywhere. Server components pushing logic back to where data already lives. Islands architecture letting you hydrate only what needs to be interactive. Old ideas coming back with new packaging because, annoyingly, they still work.

The common thread isn’t nostalgia. It’s restraint.

Smaller frontends. Smarter servers. Fewer moving parts per feature.

For a lot of teams, the win isn’t performance benchmarks or bundle size charts. It’s confidence. The confidence to change things without fear. The confidence that a new hire can understand the system without a guided tour through five libraries and three architectural diagrams.

The future looks less like picking the “right” framework and more like picking the lightest tool that solves the actual problem.

Sometimes that’s React.
Sometimes it’s HTMX.
Often, it’s a boring mix of both.

The real upgrade isn’t switching stacks.
It’s breaking the habit of reaching for complexity by default.

Choose boring, ship faster

There’s a version of this story where the conclusion is “HTMX wins” or “React was a mistake.” That version is louder. It also misses the point.

The real lesson is smaller and way less dramatic: defaults matter more than tools.

React didn’t make your app explode. Habit did. The habit of starting with a stack before understanding the problem. The habit of preparing for scale before earning it. The habit of assuming complexity equals professionalism.

HTMX isn’t special because it’s new. It’s special because it makes you pause. It asks, “Do you actually need more than this?” And sometimes the honest answer is no.

Choosing boring tech isn’t settling. It’s opting out of unnecessary stress. It’s shipping features that are easier to reason about, easier to debug, and easier to change six months later when nobody remembers the original context.

The future of frontend isn’t about being clever.
It’s about being deliberate.

Build the simplest thing that works.
Then stop.

Helpful resources

Top comments (0)