Every time a Rails-shop founder asks me to scope a B2B SaaS dashboard, the same question surfaces around hour three: “should we just do this in React?” It’s a reasonable question. The honest answer is more boring than either camp wants. This post is the rubric I actually use — the one I’d hand a non-technical founder before we sign anything — for choosing between Hotwire and a React SPA on top of a Rails API. No “it depends” hand-waving. A small set of decision criteria, the failure modes I watch for, and a sketch of what each architecture looks like when it’s working. If you came here for a verdict, you’ll get one. It just isn’t the same verdict for every product.
What we’re actually comparing
Hotwire is the umbrella for Turbo and Stimulus — ships with Rails 7+, renders HTML on the server, and uses HTML-over-the-wire updates plus small Stimulus controllers for client behavior. The “framework” is mostly a few data- attributes and a WebSocket channel.
React-on-Rails-API means a separate frontend bundle (Vite, Next.js, whatever) talking to Rails via JSON, usually with React Query or RTK for data fetching, and either a session cookie or a token-based auth flow.
I’m specifically scoping this to B2B SaaS dashboards: authenticated, internal-facing, data-heavy screens. Not marketing pages. Not consumer-grade interactive experiences. Not anything that needs to run offline. The dashboard category covers most of what I get hired to build — CRUDdy admin, multi-tenant settings, table/chart-heavy reporting, internal tools — and it’s the category where the two stacks genuinely compete.
A rubric, not a religion
The choice usually comes down to five questions. I score each one honestly before I write any code.
| Question | Leans Hotwire | Leans React |
|---|---|---|
| How interactive is the busiest screen? | Forms, lists, drill-downs | Drag-drop, canvases, live collaborative editing |
| Who maintains it in 18 months? | Backend-leaning team or solo Rails dev | Dedicated frontend engineer on staff |
| How much of the UI is “lists of records”? | Most of it | A minority of it |
| Do you need a native mobile app sharing the same API? | No, or “maybe someday” | Yes, in this funding round |
| Is real-time multi-user state a core feature? | Updates and notifications | Shared cursors, presence, conflict resolution |
If three or more rows lean Hotwire, I default to Hotwire. Three or more leaning React, I default to React. Two-and-three is the only place I genuinely deliberate, and in that case I weight the second row — team composition — heaviest, because the wrong stack for the team will out-cost every other factor combined.
Two things this rubric deliberately does not include: page-load performance and bundle size. Both stacks are fine for authenticated dashboards on broadband. If your users are on flaky connections from a warehouse floor, Hotwire wins on first paint — but that’s a niche that justifies the deeper discussion, not a default tiebreaker. Don’t pick an architecture because it shaves 80kb off a bundle the user only downloads once.
Where each one quietly fails
Hotwire’s failure mode is not “it can’t be interactive enough.” It’s Stimulus controllers that should have been a frontend framework. You see it in codebases that started with one drag-drop interaction and now have a 600-line Stimulus controller managing optimistic UI state, undo history, and three nested modals. At that point you’re writing React without React’s tools. The Turbo Stream response that updates one cell becomes a Turbo Stream response that re-renders a tree, and you’ve spent the budget you saved on backend rendering re-implementing reactivity badly.
React’s failure mode is the inverse: re-implementing Rails badly in TypeScript. I’ve inherited React codebases where every Rails model has been mirrored as a TypeScript interface, every controller as a service class, every validation as a Zod schema duplicated from the Rails-side validations. The frontend repo becomes a second backend that happens not to own the database. Every feature now requires two endpoints — one in Rails, one in the Next.js BFF — and the team can’t tell you which one owns authorization logic.
The dashboard is not where the product lives. The product lives in the data model. Pick the frontend that lets the smallest number of people own that data model end-to-end.
Anti-patterns I refuse to ship: Stimulus controllers over ~150 lines. React components that fetch from more than two distinct endpoints. Any architecture where a junior developer cannot answer “where does this validation live” in under thirty seconds. If the answer is “both places,” that’s the bug.
What the Hotwire version looks like in practice
For a typical B2B dashboard — say, an admin panel for a vertical SaaS with users, organizations, billing, and a workflow engine — the Hotwire layout I reach for is roughly this:
<%# app/views/workflows/index.html.erb %>
<%= turbo_frame_tag "workflows", src: workflows_path(format: :turbo_stream) do %>
<%= render @workflows %>
<% end %>
# app/controllers/workflows_controller.rb
def update
@workflow.update!(workflow_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to @workflow }
end
end
That’s the whole pattern. Server renders, Turbo Frame swaps, Stimulus controllers handle anything that needs to feel instant — keyboard shortcuts, optimistic toggles, form niceties. A team of one Rails engineer can ship and maintain this. The data model and the rendered HTML live in the same repo, the same PR, the same deploy.
The equivalent React version would be a Rails API exposing JSON, a Next.js or Vite app consuming it, React Query for caching, and probably a generated TypeScript client to keep the API contract honest. None of that is wrong — it’s just more parts, more PRs, more deploy targets, and at least one more person on payroll. For a founder hiring a contractor to ship in 4–8 weeks, the Hotwire version is usually the only one that fits the runway. For a Series A team with three frontend engineers, the React version is the one that lets them work in parallel without stepping on each other.
What success looks like
A working Hotwire dashboard, six months in, looks like: most PRs touch one file in app/views, one in app/controllers, and occasionally a Stimulus controller. New features ship as Turbo Frames inside existing layouts. The team can describe the entire frontend in an afternoon. Page transitions feel fast because the JSON payload was never the bottleneck — the database query was, and that’s a Rails problem you already know how to solve.
A working React-on-Rails-API dashboard, six months in, looks like: API contracts versioned and documented, frontend and backend deploys decoupled, a clear story for auth that doesn’t depend on session cookies. The team has at least one engineer who genuinely owns the frontend repo and can answer questions about render performance, bundle splits, and accessibility.
The diagnostic for either: can a new engineer ship a non-trivial feature end-to-end in their first week? If yes, the architecture is working. If they spend three days asking which repo owns what, it isn’t.
When this rubric doesn’t apply
It doesn’t apply if you’re shipping a consumer product where a sub-second interaction lag costs you signups. It doesn’t apply if the dashboard is incidental — your real product is an iOS app and the web is just an admin tool. It doesn’t apply if you’re inheriting an existing codebase, in which case “what’s already there” outweighs every row in the table; rewrites burn more runway than they save. And it doesn’t apply if your team is, say, two ex-Vercel engineers who haven’t touched Rails in five years. Use what your team can defend at 2am.
A claim worth disagreeing with
Here’s the falsifiable bit: for a B2B SaaS dashboard maintained by fewer than three engineers in its first two years, choosing React over Hotwire will cost you at least one engineer-month per year in coordination overhead — schema duplication, API versioning, auth bridging, deploy choreography — that you would not have spent on the Hotwire version. I can’t prove it without your codebase, but I’d bet a discovery call on it. If you’ve measured the opposite, I’d like to see the numbers.