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.
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.
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.
| 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 | 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 |
Pick Hotwire Turbo if
Pick reactolith if