Both let your backend drive a React frontend without a JSON API in the traditional sense. They take very different routes to get there.
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.
The JSON page-prop boundary has two consequences that some teams find painful:
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.
/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.
reactolith is modelled on Hotwire Turbo, not on Inertia. The wire format is HTML, end to end:
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.
| 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 |
Pick Inertia.js if
Pick reactolith if
A rough migration path:
<ui-data-table>,
<app-billing-form>) and let
createLoader resolve it from your components folder.
new App(component, AppProvider, "#reactolith-app") — navigation, form interception and state preservation are on by default.