Skip to main content

Migrate from Inertia Rails

This guide is for Rails apps that currently use Inertia Rails with React and are evaluating a move to React on Rails. It covers three things:

  1. A concept-mapping table from Inertia idioms to their React on Rails equivalents.
  2. A verified coexistence setup: both gems in one app, so you can migrate route by route instead of all at once.
  3. An honest "when to stay on Inertia" section.

For the "why" comparison (architecture, SSR, streaming, RSC tradeoffs), see Comparing React on Rails to Alternatives. This page is the "how."

Scope note: This is the v1 of the migration guide. A full step-by-step playbook (forms, shared state, SSR cutover, removing Inertia) and a worked example app are planned as follow-ups, tracked in #3899. One mapping-table parity feature, the first-class useForm-style helper (#3872), has shipped as useRailsForm. The opinionated routing/prefetch starter (#3873) is not shipped yet — check that issue for current status. The table gives you the current answer for each.

Concept mapping

Inertia conceptReact on Rails equivalentNotes
render inertia: 'Page', props: {...} in a controllerA normal Rails view that calls react_component("Page", props: {...}) (or react_component_hash when SSR returns multiple regions — though for head tags on React 19, prefer native metadata hoisting over react_component_hash)Your controller stays a conventional Rails action rendering a view. Props are any JSON-serializable Ruby hash, same as Inertia page props.
Page components resolved by name (resolve: in createInertiaApp)Components registered with ReactOnRails.register({ Page }), or discovered automatically with auto-bundlingWith auto-bundling you skip both manual registration and manual pack tags.
useForm (form state, submit, errors, processing)useRailsForm — a first-class useForm-style hook (data/setData, per-field errors, processing, wasSuccessful, submit verbs) that posts to a plain Rails controller, with automatic CSRF and 422 model-error mapping via the ReactOnRails::Controller::FormResponders concern. (Interim, still supported: fetch + ReactOnRails.authenticityHeaders().)A near-direct port of Inertia's useForm. Shipped via #3872; v1 is fetch-based. See Forms and Mutations.
<Link> (SPA navigation), prefetchInterim: plain <a> full-page navigation between Rails routes, or client-side routing within a section via React Router or TanStack Router. Planned: an opinionated SSR-friendly routing + prefetch starter is tracked in #3873React on Rails does not impose a router. Full-page navigation is also how you cross the Inertia↔React on Rails boundary during an incremental migration (verified below).
Shared data (inertia_share)railsContext for request/app-wide context (extend it via config.rendering_extension), plus per-page props from your viewrailsContext is only passed to render functions — a plain registered component receives just its props, so register the page as a render function (or copy shared values into per-page props). There is no usePage() equivalent for nested components: pass shared values down via props/React Context from the render function, or use the Redux store. For shared client state across multiple components on one page, there is also the Redux store API.
Deferred / lazy / merge propsOSS: fetch from the component after mount (standard React data fetching). Pro: async props with streaming SSR (stream_react_component_with_async_props) stream slow data into a server-rendered page; RSC moves data fetching into server componentsInertia's deferred props resolve client-side after the page visit. Pro streaming starts HTML immediately and streams the slow parts — same goal, stronger SSR story.
Partial reloadsComponent-local data refetching (the component owns its data refresh; no protocol-level partial visit needed). With Pro RSC, navigation can refetch only the RSC payload instead of a full pageThere is no direct protocol equivalent in React on Rails; data refresh is ordinary React, not a framework visit.
SSR via the Inertia/Vite SSR server (sidecar Node process)OSS: react_component(..., prerender: true) using ExecJS server rendering — no separate process to deploy. Pro: the Node renderer (dedicated Node process) and streaming SSRInertia SSR is a single renderToString pass. ExecJS rendering (pooled JS contexts inside the Ruby process) is typically slower than a dedicated Node SSR process for large pages — measure before relying on it; the Pro Node renderer is the performance path. Pro can also stream (renderToPipeableStream) and supports RSC; see the comparison page for details.
Persistent layouts (Page.layout = ...)Ordinary React composition inside your registered component; the Rails layout (ERB) owns the document shellDuring coexistence, both stacks share the same Rails layout conventions (see below).
CSRF handling (Inertia's automatic XSRF cookie/axios integration)Rails csrf_meta_tags + ReactOnRails.authenticityHeaders() on mutating requestsStandard Rails forms (form_with) also keep working — Rails owns the page.

Coexistence: run both gems during the migration (verified)

The migration-enabling claim is that inertia_rails and react_on_rails can live in the same app while you move route by route. We verified this in a minimal scratch app (2026-06-11), with one page rendered by each stack and full-page navigation across the boundary in a real browser.

Verified versions:

ComponentVersion
Ruby / Rails3.4.6 / 8.1.3
react_on_rails gem / react-on-rails npm16.6.0 / 16.6.0
inertia_rails gem3.21.2
@inertiajs/react npm2.3.26 (see version-pairing gotcha below)
shakapacker gem / npm9.7.0 / 9.7.0
React / ReactDOM19.2.7

The setup:

  • One Gemfile with both gems: gem "react_on_rails", gem "inertia_rails", gem "shakapacker".
  • One Shakapacker install compiles packs for both stacks. The Inertia entry point is just another pack (createInertiaApp bootstrapping @inertiajs/react); the React on Rails entry point registers components with ReactOnRails.register.
  • An Inertia route: a controller action with render inertia: "InertiaPage", props: {...}, using a Rails layout that loads the Inertia pack.
  • A React on Rails route: a conventional controller action whose ERB view calls react_component("HelloFromReactOnRails", props: {...}, prerender: false), using a layout that loads the React on Rails pack.
  • Each page contains a plain <a> link to the other.

What was verified (in a real browser, via Playwright):

  • Both gems boot together; no initializer or engine conflicts.
  • The React on Rails page mounts client-side with the props passed from the view helper.
  • The Inertia page mounts client-side from the data-page payload emitted by inertia_rails, with the props passed from the controller.
  • Full-page navigation across the boundary works in both directions (React on Rails page → Inertia page → back).
  • Both layouts emit csrf_meta_tags from the same Rails session — the two stacks share Rails session and CSRF infrastructure.

Gotchas found during verification:

  • Pin @inertiajs/react to the major version your inertia_rails gem supports. With inertia_rails 3.21.2, @inertiajs/react 3.4.0 failed to boot (TypeError: Cannot read properties of null (reading 'component') inside createInertiaApp); pinning to @inertiajs/react ^2 (2.3.26) worked. If your existing app already runs, keep its existing client version during the migration.
  • This was verified with full-page (<a>) navigation across the boundary. Inertia's <Link> SPA visits only work between Inertia pages; links that leave the Inertia section of the app should be regular anchors.
  • The scratch app used webpack-bundled Inertia via Shakapacker. If your Inertia app uses Vite, you can run Vite and Shakapacker side by side during the migration, but that combination was not part of this verification.

The resulting migration strategy is route-by-route: keep render inertia: actions as they are, and convert one low-risk route at a time to a conventional Rails view + react_component. Page components are plain React in both stacks, so most component code moves unchanged — what changes is how props arrive (view helper instead of the Inertia page object) and how navigation/forms talk to Rails (see the mapping table).

When to stay on Inertia

React on Rails is not the right answer for every Inertia app. Staying put is reasonable when:

  • You're a small CRUD app with no SSR, SEO, or streaming needs. Inertia's client-rendered model with controller-provided props is a great fit there, and the migration buys you little.
  • Inertia-specific form features beyond v1. React on Rails now ships useRailsForm, a close useForm equivalent, so basic form ergonomics are no longer a reason to stay. But v1 is fetch-based and defers a few Inertia niceties (file-upload progress, transform, recentlySuccessful); if your app leans on those, check the Forms and Mutations guide for current coverage first.
  • You rely heavily on Inertia's navigation conveniences<Link> visits, prefetching, scroll restoration, and flash-message-over-redirect conventions. React on Rails leaves routing choices to you; until #3873 ships an opinionated starter, replicating that polish is your code.
  • You use Inertia's multi-framework support (Vue or Svelte pages). React on Rails is React-only.

The reasons to move are the ones Inertia architecturally can't offer: streaming SSR, React Server Components, component-level code splitting with SSR, fragment caching, and incremental React adoption inside existing ERB views — see the comparison page for that side of the decision.