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

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

Translations

If you’re interested in translating or adapting this post, please contact us first.

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.

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.

Despite the fact that inertia_rails has been out there for a while now, it’s still not as popular as other solutions like Hotwire or StimulusReflex. Yet, we believe that Inertia.js is a great fit for Rails applications and it covers a lot of use cases where you need a full-featured JavaScript framework.

We also want to make Inertia.js more popular within the Rails community; to that end, we’ve started the inertia_rails-contrib project. This project is intended to provide both Rails-specific community documentation and a set of tools for integrating Inertia.js into Rails applications.

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-contrib generators.

Think of this article as a quick overview: we won’t dive too deep; we’ll just will share some links to the documentation. At the end, we’ll discuss the future of 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 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-contrib generators. First, we’ll set up a new Rails application using the Vite frontend build tool:

rails new inertia_rails_example --skip-js

cd inertia_rails_example

bundle add vite_rails
bundle exec vite install

Next, you can optionally setup Tailwind CSS. First, set the type field in the package.json file to module:

{
  "type": "module",
  // ...
}

Then, install Tailwind CSS:

npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Tailwind CSS requires some additional setup in the tailwind.config.js file:

module.exports = {
  content: [
    './public/*.html',
    './app/helpers/**/*.rb',
    './app/frontend/**/*.{js,jsx,ts,tsx,svelte,vue}',
    './app/views/**/*.{erb,haml,html,slim}'
  ],
  variants: {
    extend: {},
  },
  plugins: [],
}

Add the following lines to app/frontend/entrypoints/application.css:

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

Don’t forget to include the vite_stylesheet_tag in the app/views/layouts/application.html.erb file:

<%= vite_stylesheet_tag 'application' %>

Our Rails application is now ready for Inertia.js integration! Let’s install the inertia_rails-contrib gem:

bundle add inertia_rails-contrib

bin/rails generate inertia:install

The generator will ask you to choose a frontend framework; we’ll select React.

bin/rails generate inertia:install

Installing Inertia's Rails adapter
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/inertia @inertiajs/react react react-dom from "."

added 6 packages, removed 42 packages, and audited 69 packages in 8s

18 packages are looking for funding
  run `npm fund` for details

2 moderate severity vulnerabilities

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.
         run  npm add --save-dev @vitejs/plugin-react from "."

added 58 packages, and audited 127 packages in 6s

22 packages are looking for funding
  run `npm fund` for details

2 moderate severity vulnerabilities

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.
Adding Vite plugin for react
      insert  vite.config.ts
     prepend  vite.config.ts
Add "type": "module", to the package.json file
        gsub  package.json
Copying inertia.js into Vite entrypoints
      create  app/frontend/entrypoints/inertia.js
Adding inertia.js 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
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'
Copying framework related files
      create  app/frontend/pages/InertiaExample.jsx
Inertia's Rails adapter successfully installed

That’s it! The installation generator has set up the Inertia.js Rails adapter, installed the necessary NPM packages, configured Vite, and created an example page. At this point, you can start the Rails server and navigate to /inertia-example. 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:

class InertiaExampleController < ApplicationController
  def index
    render inertia: "InertiaExample", props: {
      name: params.fetch(:name, "World")
    }
  end
end

Note that the controller uses the inertia method to render the Inertia.js page. The props 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 the name parameter from the URL. To find out more, check the Inertia.js Rails documentation on the inertia method.

The React component is located in the app/frontend/pages/InertiaExample.jsx file:

import { Head } from '@inertiajs/react'
import { useState } from 'react'

import reactSvg from '/assets/react.svg'
import inertiaSvg from '/assets/inertia.svg'
import viteRubySvg from '/assets/vite_ruby.svg'

import cs from './InertiaExample.module.css'

export default function InertiaExample({ name }) {
  const [count, setCount] = useState(0)

  return (
    <>
      <Head title="Inertia + Vite Ruby + React Example" />

      <div className={cs.root}>
        <h1 className={cs.h1}>Hello {name}!</h1>

        {/*<div...>*/}

        <h2 className={cs.h2}>Inertia + Vite Ruby + React</h2>

        {/*<div className="card"...>*/}
        {/*<p className={cs.readTheDocs}...>*/}
      </div>
    </>
  )
}

The component accepts the name prop passed from the controller. You can update the name parameter in the URL to see the changes in the component.

Other than that, our page is a regular React component that uses the useState hook to manage the count state.

Inertia.js generators

To make Inertia.js integration even easier, we’ve added a set of generators to the inertia_rails-contrib 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/20240618171615_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/Post
      create      app/frontend/pages/Post/Index.jsx
      create      app/frontend/pages/Post/Edit.jsx
      create      app/frontend/pages/Post/Show.jsx
      create      app/frontend/pages/Post/New.jsx
      create      app/frontend/pages/Post/Form.jsx
      create      app/frontend/pages/Post/Post.jsx
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/posts_controller_test.rb
      create      test/system/posts_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.

Inertia CRUD demo

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.

We’ll start with the edit and update actions in the app/controllers/posts_controller.rb file:

class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy ]

  inertia_share flash: -> { flash.to_hash }

  # GET /posts/1/edit
  def edit
    render inertia: 'Post/Edit', props: {
      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

First, let’s examine the inertia_share helper. We use it to add flash messages to all the Inertia.js responses; displaying the flash[:notice] messages in the React components.

The edit action uses render :inertia to render the Post/Edit page with the serialized post data. All frontend components (pages) are located in the app/frontend/pages/Post 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.)

Next, let’s take a look at the app/frontend/pages/Post/Edit.jsx component:

import { Link, Head } 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}
          onSubmit={(form) => {
            form.transform((data) => ({ post: data }))
            form.patch(`/posts/${post.id}`)
          }}
          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. Just like default Rails scaffold, we generate a form component that handles form submission:

import { useForm } from '@inertiajs/react'

export default function Form({ post, onSubmit, submitText }) {
  const form = useForm({
    title: post.title || '',
    body: post.body || '',
    published_at: post.published_at || '',
  })
  const { data, setData, errors, processing } = form

  const handleSubmit = (e) => {
    e.preventDefault()
    onSubmit(form)
  }

  return (
    <form onSubmit={handleSubmit} className="contents">
      <div className="my-5">
        <label htmlFor="title">Title</label>
        <input
          type="text"
          name="title"
          id="title"
          value={data.title}
          className="block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full"
          onChange={(e) => setData('title', e.target.value)}
        />
        {errors.title && (
          <div className="text-red-500 px-3 py-2 font-medium">
            {errors.title.join(', ')}
          </div>
        )}
      </div>

      {/* ... */}
    </form>
  )
}

The Form component uses the useForm hook from @inertiajs/react to handle form state and submission. This hook plays nice with Rails defaults and allows you to submit forms and show validation errors without writing a lot of boilerplate code.

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 believe it has a bright future in the Rails world; it provides a simple and elegant way to build modern web applications without the complexity of client-side routing and APIs. With this project, our goal is to make Inertia.js more popular and share the tools and resources needed to build great applications.

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 and any tips or tricks you have for working with Inertia.js! Got any best practices, tips, or tricks? Whether you just want to go deeper with Inertia.js or contribute, come join us in the inertia_rails-contrib project!

Changelog

1.0.1 (2024-06-29)

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

Let us migrate you to the best tooling

You will feel improvement in velocity and developer happiness in as little as one week with our expert.

Book a call
Launch with Martians

Let's solve your hard problems

Martians at a glance
18
years in business

We're experts at helping developer products grow, with a proven track record in UI design, product iterations, cost-effective scaling, and much more. We'll lay out a strategy before our engineers and designers leap into action.

If you prefer email, write to us at surrender@evilmartians.com