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

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

Topics

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.
Schedule call

Irina Nazarova CEO at Evil Martians

Schedule 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.

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, skipping the default JavaScript and asset pipeline setup:

rails new inertia_rails_example --skip-js --skip-asset-pipeline

cd inertia_rails_example

Next, we’ll install the inertia_rails-contrib gem and run the installation generator:

bundle add inertia_rails-contrib

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 "."
Vite Rails successfully installed

Would you like to install Tailwind CSS? (y/n) y
Installing Tailwind CSS
         run  npm add tailwindcss postcss autoprefixer @tailwindcss/forms @tailwindcss/typography @tailwindcss/container-queries --silent from "."
      create  tailwind.config.js
      create  postcss.config.js
      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 react react-dom @vitejs/plugin-react --silent from "."
Adding Vite plugin for react
      insert  vite.config.ts
     prepend  vite.config.ts
Copying inertia.js entrypoint
      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
        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'
Copying page assets
      create  app/frontend/pages/InertiaExample.jsx
      create  app/frontend/pages/InertiaExample.module.css
      create  app/frontend/assets/react.svg
      create  app/frontend/assets/inertia.svg
      create  app/frontend/assets/vite_ruby.svg
Copying bin/dev
      create  bin/dev
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, 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/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.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).
Schedule 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!