Inertia.js in Rails: a new era of effortless integration

Cover for Inertia.js in Rails: a new era of effortless integration

Translations

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.
Book a call

Irina Nazarova CEO at Evil Martians

Book a call

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:

  1. 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 a data-page attribute (or a special <script> element) containing the initial page’s data in JSON format.

  2. 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 the X-Inertia header. (Another option is to use Inertia’s router to navigate to a new page programmatically.)

  3. Server Response: With the X-Inertia header 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:install

The 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 installed

Note 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_unit

The 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]) }
end

Earlier, 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

  #...
end

The 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-contrib has been upstreamed into the core inertia_rails gem.

1.1.0 (2024-08-24)

  • Use improved installation generator from inertia_rails-contrib v0.2.0.

1.0.1 (2024-06-29)

  • Add a note about "type": "module" in the package.json file.
  • Fix other typos (thanks to Alan Quimbita).
Book a call

Irina Nazarova CEO at Evil Martians

Playbook.com, Stackblitz.com, Fountain.com, Monograph.com–we joined the best-in-class startup teams running on Rails to speed up, scale up and win! Solving performance problems, shipping fast while ensuring maintainability of the application and helping with team upskill. We can confidently call ourselves the most skilled team in the world in working with startups on Rails. Curious? Let's talk!