In her RailsConf Keynote “Startups on Rails in 2024” our CEO Irina Nazarova pointed out the #1 request from young Rails startups–proper React support in the Rails community. Accordingly, we recently unveiled Turbo Mount, which simplifies cases when your app needs just a couple of highly interactive components. But today, we’re going one step further by showing how to simply integrate Inertia.js with Rails.
Hotwire and Inertia.js are both great tools for building modern web applications with Rails. However, they have different use cases and target audiences:
- Hotwire is a set of tools that enable server-side rendering of HTML and partial updates of the page using Turbo Streams and Turbo Frames. It’s a great choice for developers who want to enhance their server-rendered Rails applications with interactivity and real-time updates without writing a lot of JavaScript code. For cases where an application requires one or two highly interactive components, Turbo Mount can be used to mount a React, Vue, or Svelte component directly into a part of a classic HTML page.

Irina Nazarova CEO at Evil Martians
Inertia.js is a protocol and a set of libraries that enables a complete transition from traditional HTML templates to the use of React, Vue, Svelte, or other frontend framework components as the building blocks for the view layer. This approach is well-suited for teams proficient in frontend frameworks who want to use their skills to create more dynamic and responsive user interfaces, while still maintaining traditional server-driven routing and controllers. This setup eliminates the need for client-side routing, APIs, or the extensive JavaScript boilerplate commonly associated with having separate frontend and backend applications.
The inertia_rails gem was created back in 2019 by Brandon Shar and Brian Knoles, and has been steadily maintained ever since. While it’s not as widely adopted as Hotwire or StimulusReflex, we believe Inertia.js is a great fit for Rails applications—especially when you need a full-featured JavaScript framework.
We want to help grow Inertia.js within the Rails community. The inertia_rails-contrib project started as a separate gem, bringing enhanced tools and Rails-specific documentation to the ecosystem. It has since been upstreamed into the core project, and we’ve recently joined the maintainers team. With Brandon and Brian’s support, we’ve achieved feature parity with the Laravel adapter, including Inertia 2.0 support for once, scroll, and merge props, flash data, along with Rails-specific improvements like a new render syntax, environment variables support, and much more.
For teams who want to hit the ground running, we’ve released three official starter kits modeled after Laravel’s approach:
In this article, we’ll first take a closer look at how Inertia.js works. Then, we’ll see how to integrate it into a Rails application using inertia_rails generators.
Think of this post as a quick overview: we won’t dive too deep; as we hit our points, we’ll share some links to the documentation for further reading. At the end, we’ll discuss what’s next for Inertia.js inside the Rails ecosystem.
How Inertia.js works
To work with Inertia.js, our server-side application needs to implement the Inertia.js protocol. This protocol is based on the idea of serving two types of requests: classic HTML requests and Inertia.js requests. Let’s discuss how this works in more detail:
-
Initial Page Load: When a user first visits the application, the server returns a complete HTML response, just like a traditional server-rendered Rails application. This HTML response includes the necessary assets and a special root
<div>element with adata-pageattribute (or a special<script>element) containing the initial page’s data in JSON format. -
Subsequent Page Visits: Inertia.js provides a
<Link>component that acts as a replacement for the standard<a>tag. When a user clicks on a<Link>, Inertia.js sends an AJAX request to the server with theX-Inertiaheader. (Another option is to use Inertia’srouterto navigate to a new page programmatically.) -
Server Response: With the
X-Inertiaheader in place, the Rails application recognizes the Inertia request and returns a JSON response containing the name of a page component to be rendered, the necessary data for that component, a new page URL, and finally, version of the current asset build. Inertia.js uses that data to update the page content and the URL in the browser’s address bar without a full page refresh.
The idea behind Inertia.js is extremely simple; there is no need for Redux or a REST API—by default, every request just returns a JSON response with all the data needed to render a page.
Inertia.js Rails integration
Let’s see how to start a new Rails application with Inertia.js using the inertia_rails generators. First, we’ll set up a new Rails application, skipping the default JavaScript and asset pipeline setup:
rails new inertia_rails_example --skip-js
cd inertia_rails_example
Next, we’ll install the inertia_rails gem and run the installation generator:
bundle add inertia_rails
bin/rails generate inertia:installThe generator will install the Vite frontend build tool, optionally installing Tailwind CSS, and asking you to choose a frontend framework; we’ll select React.
$ bin/rails generate inertia:install
Installing Inertia's Rails adapter
Could not find a package.json file to install Inertia to.
Would you like to install Vite Ruby? (y/n) y
run bundle add vite_rails from "."
Vite Rails gem successfully installed
run bundle exec vite install from "."
Would you like to use TypeScript? (y/n) n
Vite Rails successfully installed
Adding package manager install to bin/setup
insert bin/setup
Would you like to install Tailwind CSS? (y/n) y
Installing Tailwind CSS
run npm add tailwindcss @tailwindcss/vite @tailwindcss/forms @tailwindcss/typography --silent from "."
prepend vite.config.ts
insert vite.config.ts
create app/frontend/entrypoints/application.css
Adding Tailwind CSS to the application layout
insert app/views/layouts/application.html.erb
Adding Inertia's Rails adapter initializer
create config/initializers/inertia_rails.rb
Installing Inertia npm packages
What framework do you want to use with Inertia? [react, vue, svelte] (react)
run npm add @inertiajs/react@latest @vitejs/plugin-react react react-dom vite@latest --silent from "."
Adding Vite plugin for react
insert vite.config.ts
prepend vite.config.ts
Copying inertia.jsx entrypoint
create app/frontend/entrypoints/inertia.jsx
Copying InertiaController
create app/controllers/inertia_controller.rb
Adding inertia.jsx script tag to the application layout
insert app/views/layouts/application.html.erb
Adding Vite React Refresh tag to the application layout
insert app/views/layouts/application.html.erb
gsub app/views/layouts/application.html.erb
Copying example Inertia controller
create app/controllers/inertia_example_controller.rb
Adding a route for the example Inertia controller
route get 'inertia-example', to: 'inertia_example#index'
route root 'inertia_example#index'
Copying page assets
create app/frontend/pages/inertia_example/index.module.css
create app/frontend/assets/rails.svg
create app/frontend/assets/react.svg
create app/frontend/assets/inertia.svg
create app/frontend/assets/vite_ruby.svg
create app/frontend/pages/inertia_example/index.jsx
Copying bin/dev
conflict bin/dev
Overwrite /Users/skryukov/EvilMartians/inertia_rails_example/bin/dev? (enter "h" for help) [Ynaqdhm] y
force bin/dev
Adding redirect to localhost
route
# Redirect to localhost from 127.0.0.1 to use same IP address with Vite server
constraints(host: "127.0.0.1") do
get "(*path)", to: redirect { |params, req| "#{req.protocol}localhost:#{req.port}/#{params[:path]}" }
end
Inertia's Rails adapter successfully installedNote that you might need to manually create the bin/vite binstub by running bundle binstub vite_ruby to fix the vite_ruby installation script issue when used with Bundler 4.
That’s it! The installation generator has set up the Inertia.js Rails adapter, installed the necessary NPM packages, installed and configured Vite and Tailwind CSS, and created an example page. At this point, you can start the Rails server by running bin/dev and navigate to http://localhost:3100. You should see the Inertia.js page with the React component.
Inertia install generator in action
Let’s take a closer look at the generated controller and component. The controller is located in the app/controllers/inertia_example_controller.rb file:
# frozen_string_literal: true
class InertiaExampleController < InertiaController
def index
render inertia: {
rails_version: Rails.version,
ruby_version: RUBY_DESCRIPTION,
rack_version: Rack.release,
inertia_rails_version: InertiaRails::VERSION,
}
end
end
Note that the controller uses the inertia method to render the Inertia.js page. The following hash contains the props that will be passed to the React component. It’s a good practice to use serializers to prepare the props for the frontend, but for simplicity, we’re just passing parameters right from the controller. To find out more, check the Inertia.js Rails documentation on the inertia method.
By default, Inertia Rails follows Rails conventions for naming the Inertia.js page components, so the page component for the controller action InertiaExampleController#index is located at app/frontend/pages/inertia_example/index.jsx:
import { Head } from '@inertiajs/react'
import { version as react_version } from 'react'
import railsSvg from '/assets/rails.svg'
import inertiaSvg from '/assets/inertia.svg'
import reactSvg from '/assets/react.svg'
import cs from './index.module.css'
export default function InertiaExample({ rails_version, ruby_version, rack_version, inertia_rails_version }) {
return (
<div className={cs.root}>
<Head title="Ruby on Rails + Inertia + React" />
<nav className={cs.subNav}>{/*...*/}</nav>
<div className={cs.footer}>
<div className={cs.card}>
<p>
Edit <code>app/frontend/pages/inertia_example/index.jsx</code> and save to test <abbr title="Hot Module Replacement">HMR</abbr>.
</p>
</div>
<ul>
<li>
<ul>
<li><strong>Rails version:</strong> {rails_version}</li>
<li><strong>Rack version:</strong> {rack_version}</li>
</ul>
</li>
<li><strong>Ruby version:</strong> {ruby_version}</li>
<li>
<ul>
<li><strong>Inertia Rails version:</strong> {inertia_rails_version}</li>
<li><strong>React version:</strong> {react_version}</li>
</ul>
</li>
</ul>
</div>
</div>
)
}The component accepts props passed from the controller. You can update the props in the controller to see the changes in the component without reloading the page.
Other than that, our page is a regular React component.
Inertia.js generators
To make Inertia.js integration even easier, we’ve added a set of generators to the inertia_rails gem. Let’s see how to generate a new Inertia.js resource:
bin/rails generate inertia:scaffold Post title:string body:text published_at:datetime
invoke active_record
create db/migrate/20251231000042_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
invoke resource_route
route resources :posts
invoke scaffold_controller
create app/controllers/posts_controller.rb
invoke inertia_tw_templates
create app/frontend/pages/posts
create app/frontend/pages/posts/index.jsx
create app/frontend/pages/posts/edit.jsx
create app/frontend/pages/posts/show.jsx
create app/frontend/pages/posts/new.jsx
create app/frontend/pages/posts/form.jsx
create app/frontend/pages/posts/post.jsx
invoke resource_route
invoke test_unit
create test/controllers/posts_controller_test.rb
invoke helper
create app/helpers/posts_helper.rb
invoke test_unitThe generator creates a new Post resource with the specified attributes. It generates the model, controller, views, and frontend components.
Since the generator creates a lot of files, let’s just take a look at form handling in the generated controller and leave the rest for you to explore.
To ease future development, we generate a special base controller InertiaController that is used to share data between all pages:
# frozen_string_literal: true
class InertiaController < ApplicationController
# Share data with all Inertia responses
# see https://inertia-rails.dev/guide/shared-data
# inertia_share user: -> { Current.user&.as_json(only: [:id, :name, :email]) }
endEarlier, we used inertia_share to add flash messages to all the Inertia.js responses, displaying the flash[:notice] messages in the components, but since then, Inertia.js has introduced a new way to share flash data. So now, InertiaController only contains a placeholder for the inertia_share helper.
Next, let’s take a look at the edit and update actions in the app/controllers/posts_controller.rb file:
class PostsController < InertiaController
before_action :set_post, only: %i[ show edit update destroy ]
# GET /posts/1/edit
def edit
render inertia: {
post: serialize_post(@post)
}
end
# PATCH/PUT /posts/1
def update
if @post.update(post_params)
redirect_to @post, notice: "Post was successfully updated."
else
redirect_to edit_post_url(@post), inertia: { errors: @post.errors }
end
end
#...
endThe edit action uses render :inertia to render the posts/edit page with the serialized post data. All PostsController frontend components (pages) are located in the app/frontend/pages/posts directory.
Finally, the update action updates the post and redirects to the post show page if the update is successful. Note that if there are any validation errors, it doesn’t raise an error, and instead redirects back to the edit page with the errors serialized in the inertia key.
(Another interesting topic worth your time is error handling docs in general.)
And now, let’s take a look at the app/frontend/pages/posts/edit.jsx component:
import { Head, Link } from '@inertiajs/react'
import Form from './form'
export default function Edit({ post }) {
return (
<>
<Head title="Editing post" />
<div className="mx-auto md:w-2/3 w-full px-8 pt-8">
<h1 className="font-bold text-4xl">Editing post</h1>
<Form
post={post}
action={`/posts/${post.id}`}
method="patch"
submitText="Update Post"
/>
<Link
href={`/posts/${post.id}`}
className="ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium"
>
Show this post
</Link>
<Link
href="/posts"
className="ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium"
>
Back to posts
</Link>
</div>
</>
)
}Notice that we use Link from @inertiajs/react to navigate to other pages without a full page reload. The generated code uses the Inertia Form component for cleaner, more readable form handling:
import { Form as InertiaForm } from '@inertiajs/react'
export default function Form({ post, submitText, ...formProps }) {
return (
<InertiaForm
transform={data => ({ post: data })}
className="contents"
{...formProps}
>
{({ errors, processing }) => (
<>
<div className="my-5">
<label htmlFor="title">Title</label>
<input
type="text"
name="title"
id="title"
defaultValue={post.title}
className="block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full"
/>
{errors.title && (
<div className="text-red-500 px-3 py-2 font-medium">
{errors.title.join(', ')}
</div>
)}
</div>
{/*...*/}
</>
)}
</InertiaForm>
)
}The Form component wraps the form state management, making the code more declarative and easier to follow. It plays nice with Rails defaults and allows you to submit forms and show validation errors without writing a lot of boilerplate code and any useState or useEffect hooks.
And in general, there is so much more to explore related to Inertia.js and Rails integration, so we again encourage you to check the full Inertia.js Rails documentation to learn more about the available features and best practices.
The future of Inertia.js in the Rails ecosystem
There are a lot of exciting things happening in the Rails ecosystem right now: Hotwire, Turbo, and StimulusReflex are gaining popularity and changing the way we build.
…And Inertia.js is another great addition to the Rails toolkit. We’ve come a long way since first publishing this guide in 2024: Inertia Rails docs, Inertia 2.0 support, official starter kits, cleaner scaffolds, and deeper Rails integration. But there’s more on the horizon.
Improved flash data handling just landed, making Inertia Rails (once again) fully aligned with all available Inertia.js features. And we’re working on frictionless TypeScript integration with Alba, alba-inertia and typelizer gems, but that’s a topic for another post.
If you’re interested in learning more about Inertia.js (and how it can help you build better Rails applications), head to the Inertia.js Rails documentation. And, if you’re already using Inertia.js in your Rails applications, we’d love to hear about your experience! Got questions or want to contribute? Join us at inertia_rails on GitHub.
Changelog
2.0.0 (2025-12-29)
- Install generator now tracks latest Inertia Rails defaults.
- Migrated scaffolds to Inertia Form component.
- Introduce three official starter kits: React, Vue, Svelte.
1.2.0 (2024-12-23)
inertia_rails-contribhas been upstreamed into the coreinertia_railsgem.
1.1.0 (2024-08-24)
- Use improved installation generator from
inertia_rails-contribv0.2.0.
1.0.1 (2024-06-29)
- Add a note about
"type": "module"in thepackage.jsonfile. - Fix other typos (thanks to Alan Quimbita).


