Reactolith vs Hotwire Turbo

Same philosophy — backend renders HTML, the page is morphed in place, state survives. Different runtime: Turbo morphs the DOM, reactolith morphs a React component tree.

The shared idea

Both libraries start from the same architectural commitment: your backend keeps producing full HTML pages, the way it always has. Routing stays on the server. Templates stay on the server. The client’s job is to make the experience feel app-like — intercept link clicks and form submits, fetch the next page in the background, and update the page in place so you don’t lose scroll, focus, video playback, open dialogs, or anything else that lives in the DOM.

That’s Turbo Drive. And it’s exactly what reactolith does — just for a React component tree.

Why I wrote reactolith anyway

I love Turbo. I wanted Turbo on a project where the UI was already built in React — design system components, stateful widgets, the lot. Turbo’s morphing is DOM-level: when it diffs the incoming HTML against the page, it has no idea your <dialog data-controller="modal"> is actually a React-managed component with its own state. You either re-implement those widgets in vanilla + Stimulus, or you fight the morph.

reactolith is the version of Turbo where the morph happens through React’s reconciler. The server still ships HTML; reactolith parses it, treats every kebab-cased tag (<ui-button>, <app-sidebar>) as a React component, and lets React reconcile the difference. State, refs and effects survive exactly the way they do during a normal React re-render — because that’s what it is.

Side-by-side

  reactolith Hotwire Turbo
Wire format HTML HTML
Frontend runtime React Vanilla DOM (+ Stimulus for behaviours)
How is the page morphed? React reconciler diffs the new tree against the live one Idiomorph / DOM diff
Custom components React components, resolved from kebab-case tags Custom Elements / Stimulus controllers
Partial updates Whole page is morphed in place; only changed nodes update <turbo-frame> for scoped swaps; <turbo-stream> for actions
Real-time Mercure SSE pipes HTML through the same render path Turbo Streams over Action Cable / SSE
Form interception Built in; the server can add/remove fields without losing focus Built in via Turbo Drive
Scroll restoration Built in Built in
Backend ecosystem Anything that prints HTML — Symfony, Rails, Laravel, Django, Phoenix, … Strongest with Rails; works with any backend

Turbo concepts and their reactolith equivalents

Turbo reactolith
Turbo Drive (link/form interception, page morph) Built-in router — same behaviour, applied to a React tree
<turbo-frame> for scoped swaps Not needed in the same way: the whole page morphs in place, and React state inside untouched components survives by default
<turbo-stream> over Action Cable Mercure SSE pushes HTML through the normal render path
Stimulus controllers for behaviours Plain React components — hooks, context, refs — no separate behaviour layer

When to pick which

Pick Hotwire Turbo if

  • You don’t need React. Turbo + Stimulus + a sprinkle of Custom Elements is a smaller dependency surface and a much smaller mental model.
  • You’re on Rails and want the whole Hotwire stack — Turbo, Stimulus, Strada — with first-party integration.
  • Your existing components are vanilla DOM / Stimulus controllers; rewriting them in React would just be churn.

Pick reactolith if

  • Your component library is already in React — shadcn/ui, Radix, your own design system — and you don’t want to leave that ecosystem.
  • You want React’s programming model (hooks, context, refs, suspense) for interactivity, but you don’t want a SPA + JSON API.
  • You want Turbo’s morph semantics — long-lived UI that survives every navigation and form submit — expressed through the React reconciler.
  • You want to keep the Majestic Monolith architecture but with a React frontend on top.

Read next

  • reactolith vs Inertia.js
  • How reactolith hydrates and morphs the tree
  • Mercure: real-time HTML push, like Turbo Streams