Every few months someone publishes a minimal UI library in a few hundred lines of vanilla JavaScript and the comments split into the same two camps. One side says this is all anyone ever needed. The other says wait until you have a real application.
Both camps are arguing about the wrong thing. I've spent the last few years building Magento storefronts on Hyvä, which is that argument made real: no React, no Knockout, no RequireJS, just Alpine and Tailwind. The storefronts are fast. The bundle argument is settled and it was never very interesting.
What nobody writes about is the bill. When you remove a framework you don't just remove its bytes. You take back a set of responsibilities it was handling without telling you, and you get no warning about which ones. Here are the three that cost me the most time.
1. Initialisation timing becomes your problem
React owns the lifecycle. A component mounts, effects run, and if it renders you know it initialised. That link is so reliable you stop thinking of it as a feature.
A minimal setup breaks the link, and it breaks it deliberately, because initialising every interactive component on page load is exactly the cost you were trying to avoid. So components get deferred: registered now, initialised later, on some trigger.
Hyvä does this with an x-defer attribute. The markup is on the page, the component is registered, and Alpine doesn't touch it until the trigger fires. Great for a product page with a dozen interactive widgets below the fold.
The failure mode is nasty. I once had a configurable-product size picker render as a thin strip with the label and no options in it. No console errors. The component function was defined on window. The catalogue data was fine, five salable children, the attribute mapped correctly.
Everything you'd check to diagnose a bug came back clean, because it wasn't a bug. The component simply hadn't been woken up. Its trigger never fired in that layout. It was sitting there fully registered and completely inert.
That reads exactly like a data problem or a broken build. I checked the indexer. I checked the swatch attribute. I rebuilt static content twice. The actual answer was one attribute deciding when, and nothing in the failure told me timing was the axis to look at.
With React, "the component didn't render" and "the component didn't initialise" are the same event. Without it, they're two events, and only one of them leaves evidence. Add "is this thing actually initialised?" to the top of your debugging list, before you go anywhere near the data.
2. State has to survive DOM it didn't create
The second thing you inherit is harder, and it's specific to server-rendered commerce.
In a React storefront the client owns the DOM. Cart state lives in a store, a mutation re-renders the subscribers, done. The framework's whole value proposition is that you describe state and never touch nodes.
On a server-rendered storefront, large parts of the page arrive as HTML from the server, and some of them arrive again after an interaction. Add to cart, and a section of markup is replaced wholesale by fresh HTML. Any JavaScript state attached to the old nodes goes with them.
So you need a rule about where state lives, and you need it before you write the second component, not after. Roughly:
- State the server owns (cart contents, customer group, prices) lives in a store the components read from, and gets refreshed from the server after a mutation.
- State the component owns (is this dropdown open, what's in this quantity input) lives on the component and is allowed to die with its markup.
Get that boundary wrong in the obvious direction and you get the classic bug: a mini-cart that shows the right total until you add a second item, then shows a stale one, because two components each kept their own copy and only one of them heard about the update.
React didn't solve this problem for you. It made it very hard to express the broken version, which is not the same thing but works out similarly in practice. Write the boundary down. A comment at the top of your store file naming what's server-owned is worth more than it looks.
3. Content Security Policy stops being someone else's config
This one only bites in a certain kind of project, but when it bites it invalidates a lot of code at once.
A build step gives you a CSP-friendly bundle almost by accident: your JavaScript ends up in files, so a strict script-src policy is satisfied without you thinking about it. Minimal setups tend to put behaviour in attributes, right in the markup, which is the whole ergonomic appeal: you can see what an element does by reading it.
Under a strict CSP that's a problem, because expressions evaluated at runtime look like eval to the browser. Alpine ships a CSP-compatible build precisely for this, and the tradeoff is that you can no longer write arbitrary expressions inline. Behaviour moves into registered component objects; the markup references methods and properties by name instead of evaluating logic.
It's a fine constraint. It's just one you want to know about on day one, because retrofitting it means rewriting every component you wrote in the ergonomic style. If your project is likely to need a strict policy (anything handling payment, anything where a security review is part of shipping), start in the CSP-compatible style even while nothing is enforcing it.
When the trade is worth it
It's worth it more often than framework people admit and less often than the minimal-library posts imply.
The minimal approach wins when the server already owns most of the truth. A commerce storefront renders catalogue and cart data that lives in a database and is authoritative there. Shipping a client framework to re-derive state the server already computed is paying twice. That's why Hyvä works, and it's a structural reason, not a taste one.
It loses when the client is the source of truth: a configurator, a builder, a dashboard with heavy interdependent state, anything where the interesting state exists only in the browser and needs to stay coherent across many views. There, the framework's rendering model is the product, and hand-rolling it means writing a worse React with none of the documentation.
The useful question isn't "do I need React." It's: which of these three responsibilities am I ready to own? Initialisation timing, state across server-rendered DOM, and policy constraints on where code lives. If the answer is all three, drop the framework and enjoy the speed. If you haven't thought about them, you haven't removed complexity. You've moved it somewhere with no error messages.
Source / further reading: https://pedroth.github.io/?p=post/NoNeedReact
Top comments (0)