Inertia.js in Rails: a new era of effortless integration
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.
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:
-
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-page
attribute 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-Inertia
header. (Another option is to use Inertia’srouter
to navigate to a new page programmatically.) -
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.
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.
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 thepackage.json
file. - Fix other typos (thanks to Alan Quimbita).