The React-or-Hotwire argument usually happens between two greenfield opinions. This is the other kind of datapoint: a thirteen-year-old Rails codebase at Domestika, serving millions of creative professionals, where react-on-rails was already load-bearing, and we took it out as the first item of a rescue mandate, because it was the most expensive thing in the building.
What react-on-rails actually costs
The gem’s pitch is reasonable: render React components from Rails views, keep one app. The price tag shows up elsewhere, and it isn’t in the components.
It’s in the build. Every deploy of anything paid the JavaScript build tax, whether the change touched a React view or not. It’s in the test runs that carried the same overhead. And it’s in something harder to see on a dashboard: every developer now maintains two mental models of how a view gets rendered, and every new hire learns both, plus the glue between them. The components themselves were often fine. The seam was the cost, and it was charged on every change.
That’s the kill criterion worth generalizing, and it’s the one I’d defend in any architecture review: infrastructure that taxes the whole system is a different category of problem than code that’s merely ugly. Ugly code costs you when you touch it. A toll booth costs you whether you touch it or not.
What died and what stayed
Precision matters here, because “we removed React” is not what happened. React-on-rails is glue that lets server-rendered pages pretend parts of themselves are React applications. The glue died. The views went back to being views: server-rendered Rails with Stimulus for the interactivity they actually had, which for most of them was a fraction of what a React mount point implies.
Where a real application lived, React stayed. Domestika has a standalone frontend app, genuinely application-shaped: owned state, complex interactions, its own lifecycle. Wojtek modernized it into TypeScript and kept it React, because that’s what React is for. The honest rule that fell out: views want to be views, applications want to be applications, and most of the pain in Rails frontends comes from dressing one as the other.
If you’re keeping score against the framework wars: this isn’t a Hotwire-beats-React post. It’s a seams post. The expensive thing was the glue pretending the two were one thing.
How the conversion shipped
There was no migration branch and no frontend freeze. Views converted to Stimulus in slices small enough that a bad slice was a small revert, shipped alongside normal feature work over months. Boring is the entire safety strategy: each conversion was individually unremarkable, which is what let the team keep shipping product while the architecture changed under them.
The prize wasn’t any single view. The prize was the moment the gem left webpack entirely, because that’s when the build tax stopped being charged on every deploy of everything, and the conversions were worth sequencing with that moment in mind.
That moment is measurable. Deploy time across the app dropped 40%, counted on the pipeline, traceable to the build getting lighter and to a test suite that got honest along the way (a story of its own). Nobody set out to optimize deploys.
Stimulus held, and here’s why it was enough
The doubt every team has before this conversion: surely the views are too interactive for sprinkles of Stimulus. Then you read the actual views and find that most of these views used a sliver of what React offers, at the price of the full machinery: a dropdown here, a tab switch there, a form that validates before submit. Stimulus covers that tier with a controller and some data attributes (and where a fragment needs replacing from the server, that’s Turbo’s lane in the Hotwire stack), the server keeps owning the HTML, and there’s suddenly one way views render here instead of two. The skills question answered itself along the way: the people converting views were the same people who knew React, and Wojtek, our most React-shaped engineer, worked both sides of the seam.
The cases with real client-side state are exactly the ones that belong in the standalone app. The conversion didn’t flatten Domestika’s frontend into one paradigm. It sorted everything onto the correct side of the seam, and both sides got simpler.
If you’re weighing the same kill
Three questions tell you most of what you need. What does the glue cost per deploy, measured, not felt? Toll-booth infrastructure justifies removal by arithmetic; ugly-but-quiet code doesn’t. Can you convert in shippable slices, or does your coupling force a big bang? If slices aren’t possible, fix that first, because the safety of this whole approach lives there. And for each view: is this actually an application? If yes, give it a real application home and keep your framework of choice. If no, it’s a view, and your server already knows how to render those.
We were a three-person team doing this inside a living product, alongside the rest of an eleven-month rescue. The codebase fought back exactly as much as thirteen years of decisions usually do. The kill paid for itself before the engagement ended, starting with the deploy clock, before counting what one rendering model does for everyone who touches the code after you.