Reactolith vs Inertia.js

Both let your backend drive a React frontend without a JSON API in the traditional sense. They take very different routes to get there.

The one-line difference

  • Inertia.js sends JSON page props and re-renders a single page-level React component on every visit.
  • reactolith sends HTML and morphs the existing React tree in place — the way Hotwire Turbo morphs the DOM — so component state survives across navigation.

What Inertia.js is good at

Inertia.js is a brilliant piece of design. It gives you almost the entire SPA developer experience — client-side routing, page components, partial reloads, lazy props — while letting your Rails/Laravel/Symfony controller stay the source of truth for what a page contains. If your team is comfortable with the page-as-component model and you mostly build form-driven CRUD apps, Inertia is a great fit and you should probably keep using it.

What Inertia.js gives up

The JSON page-prop boundary has two consequences that some teams find painful:

  1. The page component is replaced on every visit. That is the whole mental model. There is no morph: state inside the previous page component does not survive to the next one. A persistent layout (renderLayout) helps for the chrome, but anything inside the page — an open dialog, a half-typed form, a video element, an embedded chart with its own internal state — resets unless you hoist it into the persistent layout.
  2. You ship a parallel data contract. Every controller has to return Inertia page props in a shape the React page expects. That is still nicer than building a generic /api/v2, but it’s a contract you have to maintain, version, type, and keep in sync with the UI. URL helpers, signed URLs, flash messages, permission checks — anything your framework already renders into HTML — have to be re-projected as JSON fields.

How reactolith does it differently

reactolith is modelled on Hotwire Turbo, not on Inertia. The wire format is HTML, end to end:

  1. On a link click or form submit, reactolith fetches the next page as HTML — the same HTML the server would render for a full page load.
  2. It hands that HTML to React’s reconciler, which diffs it against the live tree and updates only what changed.
  3. Component state, focus, scroll position, open dialogs, mounted iframes — everything not touched by the diff stays exactly as it was.

Because the wire format is HTML, the controller stays the contract. There’s no page-prop type to keep in sync; if the template changes, the React tree just morphs to match.

Side-by-side

  reactolith Inertia.js
Wire format HTML (server templates) JSON page props
Frontend libraries React React, Vue, Svelte
Navigation model Morph React tree in place Replace page-level component
Component state across nav Preserved (only the diff is applied) Reset, unless lifted into a persistent layout
Backend layer to add None — your existing controllers & templates An Inertia adapter that returns page props
URL helpers / flash / signed URLs Used directly in templates Re-projected as JSON fields
Forms with server-driven changes Server can add/remove fields; React state & focus persist Re-render the page with new props
Real-time updates Mercure SSE pipes HTML through the same render path External (e.g. Echo / Pusher) wired manually
SSR You’re already doing it — the server renders HTML Optional Node SSR service

When to pick which

Pick Inertia.js if

  • You want a React/Vue/Svelte choice and a mature ecosystem with first-party adapters for Laravel and Rails.
  • The page-as-component model fits your app cleanly — most pages are independent, state doesn’t need to survive across them.
  • You’re comfortable maintaining a JSON page-prop contract between controllers and React.

Pick reactolith if

  • You specifically want React, and you want Hotwire Turbo’s morph behaviour for it.
  • You don’t want to add a JSON page-prop layer — the templates your backend already renders should be the contract.
  • You want long-lived UI — persistent sidebars, stateful tables, embedded media, dialogs — that survives every navigation and form submit untouched.
  • You like the Majestic Monolith architecture and want React on top of it without splitting the app in two.

Migrating from Inertia.js

A rough migration path:

  1. Replace your Inertia page components with regular React components, and stop returning Inertia page props from the controller. Return the page’s HTML template instead — the same one you’d render for a full page load.
  2. Wherever a page used a custom Inertia component, expose it as a kebab-cased tag (<ui-data-table>, <app-billing-form>) and let createLoader resolve it from your components folder.
  3. Drop the JSON page-prop boundary: anything you projected into JSON for the page (flash messages, permission flags, URL helpers, signed URLs) goes back into the template directly.
  4. Mount with new App(component, AppProvider, "#reactolith-app") — navigation, form interception and state preservation are on by default.

Read next

  • reactolith vs Hotwire Turbo
  • How reactolith hydrates and morphs the tree
  • Quick start